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.
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.
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.
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
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.
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:
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.
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 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().