I've been recently trying C++17's std::variant
and std::visit
, and I found it quite powerful.
I specially liked the ability to create a visitor pattern over several variant objects.
Here is one example of what I mean:
std::variant<int, float, char> v1 { 's' };
std::variant<int, float, char> v2 { 10 };
std::visit(overloaded{
[](int a, int b) { },
[](int a, float b) { },
[](int a, char b) { },
[](float a, int b) { },
[](auto a, auto b) { }, // << default!
}, v1, v2);
See https://www.bfilipek.com/2018/09/visit-variants.html for full details. In view of this, I was wondering if it'd be possible to write similar code based on polymorphic types rather than variant objects.
Think about an scenario where we're using dynamic polymorphism and parent objects for writing a generic interface. Then we want to implement a certain functionality that depends on several polymorphic types, i.e. like
void fun(IFoo* ptr_foo, IBar* ptr_bar) {
{
Foo1* child_foo = dynamic_cast<Foo1*>(ptr_foo);
Bar1* child_bar = dynamic_cast<Bar1*>(ptr_bar);
if(child_foo && child_bar) { return fun(child_foo, child_bar) }
}
// ... other cases
{
Foo1* child_foo = dynamic_cast<Foo1*>(ptr_foo);
BarN* child_bar = dynamic_cast<BarN*>(ptr_bar);
if(child_foo && child_bar) { return fun(child_foo, child_bar) }
}
// ... other cases
{
FooN* child_foo = dynamic_cast<FooN*>(ptr_foo);
BarN* child_bar = dynamic_cast<BarN*>(ptr_bar);
if(child_foo && child_bar) { return fun(child_foo, child_bar) }
}
throw std::runtime_error{};
}
I know the above is far from optimal, but just trying to make the scenario as clear as possible.
In this scenario, using a virtual function for fun
does not seem straightforward, as it depends on the type of two inputs.
Besides, we're trying to avoid virtual methods for these functionalities,
as we prefer keeping the interface for IFoo
or IBar
agnostic of these external functions.
Using a visitor pattern does not seem plausible either, for several input objects to the visiting function.
The simplest approach seems the example implementation using dynamic_cast
I showed above,
but this quickly escalates in the number of cases to write as we go from 1 to N inputs.
The std::variant
+std::visit
approach above, though, covers this scenario so cleanly and straightforwardly.
So wrapping up, our constraints/requirements are:
dynamic_cast
is fine)std::visit
approachIs this possible somehow?
I was considering writing a variadic recursive templated function, akin to std::visit
,
which would automatically generate all type cases to check for.
An example of use would be something like:
visitPolymorphic<tuple<Foo1, Foo2>,tuple<Bar1, Bar2, Bar3>>(ptr_foo, ptr_bar)
This would if-else over the different template input types, and dispatch the right call.
Any thoughts on this?
You can use std::variant
there too:
struct Foo1;
struct Foo2;
struct Foo3;
using FooVariant = std::variant<Foo1*, Foo2*, Foo3*>;
struct IFoo
{
virtual ~IFoo() = default;
FooVariant AsVariant() = 0;
// ...
};
struct Foo1 : IFoo
{
FooVariant AsVariant() override { return this;}
// ...
};
// Same for FooX
struct Bar1;
struct Bar2;
struct Bar3;
using BarVariant = std::variant<Bar1*, Bar2*, Bar3*>;
struct IBar
{
virtual ~IBar() = default;
BarVariant AsVariant() = 0;
// ...
};
struct Bar1 : IBar
{
BarVariant AsVariant() override { return this;}
// ...
};
// Same for BarX
And then
void fun(IFoo& foo, IBar& bar) {
std::visit(overloaded{
[](Foo1* a, Bar1* b) { /*..*/ },
[](Foo2* a, Bar2* b) { /*..*/ },
[](Foo3* a, auto* b) { /*..*/ },
[](auto* a, auto* b) { /*..*/ }, // << default!
},
foo.AsVariant(), bar.AsVariant()
);
}
If you don't want that virtual AsVariant()
in interface (but use dynamic_cast
), you might still have free function:
FooVariant AsVariant(IFoo& foo)
{
if (auto* p = dynamic_cast<Foo1*>(&foo)) {
return p;
}
if (auto* p = dynamic_cast<Foo2*>(&foo)) {
return p;
}
if (auto* p = dynamic_cast<Foo3*>(&foo)) {
return p;
}
throw std::runtime_error("Invalid type");
}