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?
Modern C++ Features - std::variant and std::visit - Simplify C++! std::variant is a library addition in C++17 for sum types, and std::visit is one of the ways to process the values in a std::variant.
Here's a basic example with one variant: We have a variant that represents a package with four various types, and then we use super-advanced VisitPackage structure to detect what's inside. The example is also quite interesting as you can invoke a polymorphic operation over a set of classes that are not sharing the same base type.
With std::variant we can express an object that might have many different types - like a type-safe union, all with value semantics. And then with std::visit we can call a visitor object that will invoke an operation based on the active type in the variant.
std::visit is a powerful utility that allows you to call a function over a currently active type in std::variant. It does some magic to select the proper overload, and what's more, it can support many variants at once.
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");
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With