In addition to the answer from John:
Storing different types to a single vector and the potential higher memory consumption by using std::variant
can be overcome by using a variant of pointer types like
std::vector< std::unique_ptr<A>, std::unique_ptr<B> >
I see a very big advantage on independent types and std::variant
in the fact that we don't need a common base class. Even on heterogeneous classes we can store and do something with the elements. Even if they don't have any common base class or even they do not have a common interface at all!
struct A
{
void Do() { std::cout << "A::Do" << std::endl; }
};
struct B
{
void Do() { std::cout << "B::Do" << std::endl; }
void Foo() { std::cout << "B::Foo" << std::endl; }
};
struct C
{
void Foo() { std::cout << "C::Foo" << std::endl; }
};
int main()
{
using VAR_T = std::variant< std::unique_ptr<A>, std::unique_ptr<B> >;
std::vector<VAR_T> v;
v.emplace_back( std::make_unique<A>() );
v.emplace_back( std::make_unique<B>() );
for ( auto& el: v ) { std::visit( []( auto& el ){ el->Do(); }, el ); }
// You can combine also a vector to other unrelated types which is impossible
// in case of using virtual functions which needs a common base class.
using VAR2_T = std::variant< std::unique_ptr<B>, std::unique_ptr<C> >;
std::vector<VAR2_T> v2;
v2.emplace_back( std::make_unique<B>() );
v2.emplace_back( std::make_unique<C>() );
for ( auto& el: v2 ) { std::visit( []( auto& el ){ el->Foo(); }, el ); }
// and even if some class did not provide the functionality, we can deal with it:
// -> here we try to call Do which is only in type B!
for ( auto& el: v2 ) { std::visit(
[]( auto& el )
{
if constexpr ( requires { el->Do();} )
{
el->Do();
}
else
{
std::cout << "Element did not provide function!" << std::endl;
}
}
, el ); }
}
The argument that "feature xy is less familiar to most C++ users" is a common problem with all kind of domains. If you never had seen a hammer, it might be valid to use a stone to drive the nail. Best fit designs can only be done, if we know the toolset and how to use it. And education of teams is the best investment a tec company can do.
Back to the question what to prefer:
As always it depends on the algorithm you have to implement. If run time polymorphism is fine and fits, use it. If you can't, only as example cause of non common base class, you can drive with std::variant
and std::visit
.
And for all approaches CRTP comes into play to generate mixins in all its variants.
In programming in general, there is no general "x is always better as y" rule. Designs must fit! In maintainability, resource usage ( memory, time ) usability ...