3

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

  • 2
    Note that virtual methods aren't just a less-efficient implementation of templated methods -- virtual methods let your code make decisions at run-time, so you can e.g. dynamically compose objects of various subclasses together in response to program input, and then call their virtual methods to get the corresponding behavior. So if you need that level of at-runtime flexibility, use them; if not, you can get somewhat better performance by using templates instead. – Jeremy Friesner Jul 18 '20 at 05:03
  • @JeremyFriesner Great point! Whoever writes an answer should include that in their response. –  Jul 18 '20 at 05:05
  • 1
    @superdeveloper That may not be the best example, since in the template case there is no common root class, so not much point for `general_do_run` to exist. In fact, such considerations would be precisely what you would weigh when choosing one implementation over another. See also [When to use template vs inheritance](https://stackoverflow.com/questions/7264402/when-to-use-template-vs-inheritance/7264636). – dxiv Jul 18 '20 at 05:31
  • @dxiv Right, I expect the answer to enumerate all of the things necessary for that consideration. I've seen some really quality answers on StackOverflow comparing two paradigms/options, so I figured I'd ask this in hope to get one of those answers. –  Jul 18 '20 at 05:49
  • Does this answer your question? [Dyamic vs Static Polymorphism in C++ : which is preferable?](https://stackoverflow.com/questions/9907004/dyamic-vs-static-polymorphism-in-c-which-is-preferable) – Daniel Langr Jul 18 '20 at 07:32

2 Answers2

3

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.

John Zwinck
  • 239,568
  • 38
  • 324
  • 436
  • So the only advantage to interface classes is that they can be homogeneously stored? –  Jul 18 '20 at 06:10
  • @superdeveloper: Heterogeneously, but yes. If you never store a base class pointer (or reference) and use it to invoke derived class functions, you are not getting any benefit from virtual functions. – John Zwinck Jul 18 '20 at 06:35
  • @superdeveloper That advantage goes a bit further. Think plugin systems. Runtime polymorphism lets you add functionality to your running application without any recompilation or relinking. – besc Jul 18 '20 at 07:08
1

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 ...

Klaus
  • 24,205
  • 7
  • 58
  • 113
  • Note that a variant of unique_ptrs is twice the size of a unique_ptr, so there can still be a size advantage to using `virtual` (depending on other details). – John Zwinck Jul 19 '20 at 08:00
  • 1
    @JohnZwinck: A object with virtual methods contains the vtable pointer which is exact the same size overhead as the "tag" inside the variant. – Klaus Jul 19 '20 at 13:45