Suppose you're writing the base for multiple classes. When should you make the base class have all of the dependent operations be virtual and when should the class take a template argument which is a class with the traits necessary?
e.g.
class base
{
public:
virtual void do_run() = 0;
void general_do_run()
{
// general stuff
// then
do_run();
}
};
class child: public base
{
public:
void do_run() override {}
};
vs
template<class traits>
class base
{
public:
void general_do_run()
{
traits::do_run();
}
};
struct child_traits
{
void do_run() { }
};
class child: public base<child_traits>
{
};
I've noticed that the STL seldom uses virtuals (I assume because of the overhead).
In the virtual case I can write:
std::vector<std::unique_ptr<base>>
And I can use this to store multiple different derived classes.
In the template case there is no such straightforward way to store heterogeneous derived classes in a container and do anything useful with them. You'd have to use something like this:
std::vector<std::variant<child, child2, child3>>
Which is possible, but probably consumes more space, is less familiar to most C++ users, and is not at all flexible if someone else wants to add their own derived type without modifying the vector type.
Use virtual for runtime polymorphism. Use templates or other techniques for static (compile-time) polymorphism.
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 ...
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