JUCE controls in a std::variant

I guess I’m just not familiar with std::variant yet. What’s stopping me from adding a JUCE control to a variant?

class MyClass {};
MyClass myObj;
ToggleButton tb;
std::variant<int, MyClass, ToggleButton> myVar;

myVar = 5; // OK
myVar = myObj; // OK
myVar = tb; // fails: binary ‘=’: no operator found which takes a right-hand operand of type ‘juce::ToggleButton’

Same thing for ComboBox too. Must be something silly I’m not seeing.
I’m using MSVC 2022 v17.5.5 with latest C++ setting. JUCE v7.0.5.

JUCE components are heavyweight objects. I can’t think of a time you’d ever use = to assign a component object.

What are you actually trying to achieve here?

Well, I was originally looking to get a heterogeneous collection of controls. But I didn’t want to do it by using a collection of pointers to base class Components. So something like an array of std::variant<ToggleButton, ComboBox, Slider> for example. But that doesn’t work.
The assignment example above was the simplest thing that didn’t work so I started with that.

I would probably rethink the code architecture. Variants holding different component types is a very strange choice that I would expect to introduce all kinds of weird side effects.

What’s stopping you is that you’re trying to copy a Component, and Components are marked as uncopyable. As Ben said, Components aren’t really designed for that kind of thing. I think it will work if you std::move() the Component in though.

I suppose that you’re wondering if it’s possible to use std::variant<> as an alternative form of polymorphism to the standard approach of base pointers and virtual functions, as some people these days recommend. I’ve tried this before, but it felt like an up-hill battle, so I gave up. I’d be interested to hear if anyone else has had success with it though.

1 Like

Use an ownedarray of component. Buttons, sliders, comboboxes etc all inherit component so you can dynamic_cast the component to find out what it is after the fact in other areas of your code. Take advantage of polymorphism in ui areas of your code where speed is less important.

It should work if you store components in std::unique_ptr inside std::variant, for example: std::unique_ptr<ToggleButton>.

1 Like

There is little point in adding polymorph classes into std::variant. The variant has benefit to resolve calls at compile time, opening possibilities to the optimizer. However, if the methods you call are virtual methods, you are back to square one.

The reason, why copy and move are disabled for Components is as I understand it, because the machinery expects the components to stay put after creation, because they are referenced with references and raw pointers internally. If you move now a component, everything will break.

The recommended solution that was mentioned by others before is, wrapping your components into RAII structures like

std::unique_ptr<ComponentType>` thing;
std::vector<std::unique_ptr<ComponentTytpe>> things;
juce::OwnedArray<ComponentType> moreThings;

Where OwnedArray is nothing else than a version of std::vector<std::unique_ptr<>>`with a nicer interface, but a little less secure, because it allows you to supply raw pointers.

A unique_ptr is movable but not copyable, so it should be the same situation as with the plain Component.

There is no problem with moving the unique_ptr, as long as the pointee stays put.

EDIT: just adding the info from cppreference:

the types that may be stored in this variant. All types must meet the Destructible requirements (in particular, array types and non-object types are not allowed).

So it is not the problem of std::variant, but the limitations of the pointee are imposed on the variant.

Which means you still cannot assign a std::variant<std::unique_ptr<ComponentType>>, but probably std::move it.

Thanks for the replies everyone. Yes as LiamG said I was partly trying to go for the alternate form of polymorphism. But I was also trying to avoid having to name a lot of controls and produce a lot of redundant code.

It is possible to put controls into an array and access then with indexes such as:

std::array<ToggleButton, 11> buttonCtrls;
std::array<ComboBox, 4> comboBoxCtrls;
std::array<Label, 4> comboBoxLabels;
std::array<Slider, 6> sliderCtrls;
std::array<Label, 6> sliderLabels;

Then I can set them up in a loop using statically allocated data and reduce code.
But then the next step might be to try to put these into a single array:

using ctrlType = std::variant<ToggleButton, ComboBox, Slider>;
std::array<ctrlType, 21> controls;

…and then use std::holds_alternative<> to distinguish them when needed rather than use dynamic_cast<> as you would with pointer.

But I didn’t see a way to get the buttons and combo boxes into the variants. For example, you can’t statically allocate like this:

std::array<ctrlType, 21> controls =
{
ToggleButton(),
ToggleButton(),
ComboBox(),
Slider(),

};

So I guess I’ll just back off and use the separate arrays. That does work.

Just use unique_ptr to the base class and you get the same effect:

std::array<std::unique_ptr<juce::Component>, 11> things;

Don’t design with neither, std::holds_alternative and dynamic_cast are both code smells.
Use polymorphy instead.

If you create loops over components and have to branch off depending on type inside a loop, maybe they didn’t belong into the same container in the first place…

If you want to have a variant-like approach you could write a wrapper component that can hold one of the widget types, and then have an OwnedArray of those as mentioned above.

You could then write a nice interface on your VariantComponent that would encapsulate the required dynamic-casting. E.g. you could have something like

class VariantComponent : public juce::Component
{
public:
    VariantComponent (std::unique_ptr<juce::ToggleButton>);
    VariantComponent (std::unique_ptr<juce::Slider);
    // etc.

    template<typename Value>
    void setValue(Value value)
    {
        if (auto* button = dynamic_cast<juce::Button*>(child.get()))
            button->setToggleState(value);
        else if (auto* slider = dynamic_cast<juce::Slider*>(child.get()))
            slider->setValue(value);
        // etc.
    }

private:
    std::unique_ptr<juce::Component> child;
};

That way the client of VariantComponent doesn’t need to constantly keep checking what type of component it’s currently holding.

2 Likes

Although that would work it’s more abstraction than I really needed for what I’m doing.

It is actually checking every time with the if/else and even with ugly dynamic_cast.

If you really need a generic setValue, it could work with type erasing.

The client doesn’t have to check anything since we encapsulated it, and we only need to do it in one place.
Not sure what you have against dynamic_cast :rofl: it may have an ugly interface but it’s beautiful on the inside!
If performance is your concern then compared to all the other stuff you’re going to be doing with these components, the occasional dynamic_cast is going to be negligible.


Needless tangent since OP already said this approach wasn’t right for them, but you could maybe make use of the fact most JUCE widgets hold a juce::Value to have a generic setValue()/getValue().