1

What are the advantages and disadvantages of using something like

template <typename TData>
class Base {
public:
   void foo()
   {
       static_cast<TData*>(this)->doFoo();
   }
};

class Derived : public Base<Derived>
{
    void doFoo() { \*...*\ }
};

instead of

class Base {
  virtual ~Base = default;
  virtual void foo() = 0;
};

class Derived {
  void foo() final { \*...*\ }
};

?

As far as I understand it, both approaches avoid vtable-lookups at runtime, right? So in which situations should one use the first method containing more boilerplate?

Henk
  • 826
  • 3
  • 14
  • does this answer your question? https://stackoverflow.com/questions/4173254/what-is-the-curiously-recurring-template-pattern-crtp – 463035818_is_not_an_ai Jan 19 '21 at 11:02
  • Doesn't the ```final``` keyword in the second example also lead to devirtualization? – Henk Jan 19 '21 at 11:04
  • 1
    no, the final means that you cannot override foo if you derive from Derived – StPiere Jan 19 '21 at 11:05
  • Yeah, but for example according to this article https://devblogs.microsoft.com/cppblog/the-performance-benefits-of-final-classes/, the compiler has more information and can devirtualize. – Henk Jan 19 '21 at 11:12
  • One difference is that every `Base` is a distinct type where the non-template version has a common base. But I think you are asking the wrong question to some extent. Even if the code in your example do pretty much the same thing, there are things that you can only do with CRTP and things where it's just much easier to use regular inheritance. It's not about one of them being better. – super Jan 19 '21 at 11:16

1 Answers1

2

Static polymorphism (first case) via CRTP is done at compile time and I would basically prefer it, if all classes are known at compile time. Note also that the Base itself is not a class but a class template, so Base is not a base class of Derived.

It's not just performance but also a design decision of your project if should use first or second.

In the second case Base is a base of Derived with polymorphical behaviour, so for ex. method calls on derived can be called through a pointer to Base, for ex.:

Base* b = new Derived();
b->foo(); // calls Derived::foo via vtable lookup.

Depending how foo() is invoked, compiler can devirtualize it or not, for ex:

Base* b = new Derived();
b->foo(); // cannot be devirtualized

Derived* d = new Derived();
d->foo(); // probably can be devirtualized, because compiler knows 
          // via final that none can override foo,
          // so it doesnt need to consult vtable.

Basically I prefere everything what can be done at compile time, not just because of performance, but also robustness - runtime errors are harder to handle. But it's also design decision - if the first method makes your project more complicated and small performance penalty doesn't matter, then you can go very well with second approach.

There is no "one fit's all" solution.

StPiere
  • 4,113
  • 15
  • 24
  • Why would you prefer CRTP? For me, it seems like the second approach is superior here because you can have the behavior of CRTP with the second example and have the additional feature of runtime polymorphism with the first example. If you do not use that, you do not pay for it, right? – Henk Jan 19 '21 at 11:22
  • And if you mark your functions virtual, but don't use it, as you said, then it's rather somthing wrong with your design in general, I would say. Maybe you dont need virtual at all - simple shadowing could do also, depending on your goals. – StPiere Jan 19 '21 at 11:29
  • I mean it would be nice to have something like a compile-time virtual (like constexpr virtual), but final does some part of the job and CRTP adds a lot of boilerplate that does not really declare the intent. For example, you cannot see which functions should be replaced from the declaration. – Henk Jan 19 '21 at 11:38
  • As stated, depending on your goals, you could go very well with second approach. You just need to be aware of trade-offs. Both have pro's and con's. That's what I tried to sketch in the answer. – StPiere Jan 19 '21 at 11:42
  • I agree that having the option of runtime polymorphism and no insurance of virtualiziation is a disadvantage. So both solutions are not really ideal for static polymorphism I guess. CRTP has boilerplate and not really expressing intention, final maybe not the wanted behavior (runtime polymorphism is allowed). – Henk Jan 19 '21 at 11:56