-1
\$\begingroup\$

Background

Apologies for "burying the lead." The questions are in the next section.

In my application, the purpose of inheritance is to abstract away complicated logic in the base class template, so that the writer of a derived class template doesn't need to worry about it. This logic, however, will depend on these simpler methods in the derived class.

What I have now is the base class template, something like base1 below, has the complicated function f2, and this calls the to-be-defined, user-provided function f1. I make f1 pure virtual so that gcc spits out compiler errors if someone forgot to define it.

This works great, but I've starting considering what it would be like to rewrite base1 into something like base2. From the user perspective, they don't need to worry about the weirdness of the base class inheriting stuff, so lack of familiarity with the curiously recurring template pattern wouldn't be an issue.

The downside to this would be a slightly higher burden being placed on the writer of the derived class template. The inheritance part changes from public base1<a> to something that looks a little more intimidating: public base2<derived2<a>>. So is there any speed gain that makes it worth it?

A baby example

#include <iostream>

// option 1
template<typename a>
class base1 {
public:
    virtual double f1() = 0;
    void f2() { std::cout << f1() << "\n"; }
};

template<typename a>
class derived1 : public base1<a> {
public:
    virtual double f1() { return 1.0; }
};

// option 2    
template<typename derived>
class base2 {
public:
    void f2() { std::cout << static_cast<derived*>(this)->f1() << "\n"; }   
};

template<typename a>
class derived2 : public base2<derived2<a>> {
public:
    double f1() {return 1.0;}
};


int main() {
    derived1<int> thing1;
    thing1.f2();
    derived2<int> thing2;
    thing2.f2();
    return 0;
}

Questions:

  1. Is it even polymorphism if there are no virtual functions, just pure virtual functions?
  2. Is there a vtable situation when there are just pure virtual functions? I've heard this is what makes static polymorphism faster (which might not even apply to this situation).
  3. What is different, from the compiler's perspective, about these two methods?
\$\endgroup\$
5
  • 2
    \$\begingroup\$ While this question is interesting, it seems too hypothetical. Code review is for working code that actually does something. \$\endgroup\$
    – pacmaninbw
    Jan 30, 2020 at 17:19
  • \$\begingroup\$ @pacmaninbw I have a library that uses the first pattern. This library advertises its speed, so that’s why I’m looking into this. I can share an example from there, if you’d like. \$\endgroup\$
    – Taylor
    Jan 30, 2020 at 21:10
  • \$\begingroup\$ @pacmaninbw I suspect the real example is a) too complex to put in a CR and b) possibly proprietary? The info given is sufficient to address the question at hand? See below? \$\endgroup\$ Jan 31, 2020 at 0:16
  • 2
    \$\begingroup\$ Sorry Taylor, Code Review does not allow hypothetical examples. The code you presented in its current form is not meaningfully reviewable. We only review real, working code. If you edit your question to contain your actual code, we can review it for improvements. See What topics can I ask about? for reference. \$\endgroup\$
    – L. F.
    Jan 31, 2020 at 3:25
  • \$\begingroup\$ @Taylor Check to see if anyone has asked something like this on Stack Overflow, if not that is the place to ask this question. \$\endgroup\$
    – pacmaninbw
    Jan 31, 2020 at 15:09

1 Answer 1

3
\$\begingroup\$

CRTP

Caveat: It's not quite clear what the <a> template parameter is doing/

For my understanding your option1 defeats the purpose of CRTP. That is, if it's even trying to do CRTP. It's not clear due to the template parameter <a> which appears to have no purpose. Fact is, Option1 It's working just like normal polymorphism, see below.

In the context of polymorphism the purpose of CRTP is usually to avoid vtable calls / runtime dispatch. The point is to convert runtime polymorphism to compile time polymorphism. Your Option 1 does not do that, as far as I can see.

In terms of "what's different between these 2 Options", I have put a modified version of your code on godbolt. Modified only to be able to turn on -O3 without the compiler removing the programme. The inheritance code is the same. You can clearly see that Derived1 has a vtable, and Derived2 does not.

Your Option1 also seems to lose one of the strengths of runtime polymorphism. With normal polyphism where Derived inherits from Base (no CRTP, not even your option1), all Derived classes inherit from the one base. Which means we can make a std::vector<Base*> or std::vector<unique_ptr<Base>>, ie hold pointers to many different types of objects in the same container. We can't do that with compile time / CRTP type polymorphism. But your Option1 suffers from Vtable and we still can't put them all in a vector because they don't have a common base. (ie Base<int>* and Base<D2>* won't go in the same vector. Unless <a> will one of a limited set loike int and float which can then fit a vector - again, not clear). Fact is that "proper" compile time polymorphism (see below), makes it hard/impossible to put different derived types in a collection.

I found this video helpful (link is to the CRTP code moment) to understand it, as it explores the various options, and, based on a practical example, shows their implementation and compares their performance.

Option2

Your option 2 comes closer, but overcomplicates things as far as I can see. You don't need the Derived to be templated, you just need it to be inheriting from the Base specialised with the Derived type: Base<Derived>. -- If you have an application specific need to make Derived templated, beyond CRTP, then that was not shown in your code, and yes, in that case, your Option 2 code is correct for "proper" CRTP.

Proposed solution

So by just cleaning up the above-mentioned over-complication, this works fine, doesn't it? (Note that I am showing a single Base with 2 Derived classes inheriting from it):

#include <iostream>
#include <string>

// CRTP as explained in:
// https://youtu.be/jBnIMEb2GhA?t=1098
template<typename D>
class Base {
public:
  void f2() { std::cout << static_cast<D*>(this)->f1() << "\n"; }
};

class Derived1 : public Base<Derived1> {
public:
  std::string f1() { return "derived1"; }
};


class Derived2 : public Base<Derived2> {
public:
  std::string f1() { return "derived2"; }
};


int main() {
    Derived1 thing1;
    thing1.f2();
    Derived2 thing2;
    thing2.f2();
    return 0;
}

Output:

derived1
derived2

More inheritance options, or rather...

If you are looking at a re-design, you might also want to watch this from Sean Parent, which is sort of "the next level", ie (almost) avoid inheritance altogether:

Or, the same subject, explained in a clear and recent blogpost here.

These links are exploring their "type erasure" technique not for performance reasons, but to be able to remove tight inheritance coupling between different components.

Performance motivation...be careful

Note that if you are considering CRTP for performance reasons, because it can avoid the vtable, then make sure you measure before and after. The lady in the cppcon video does it nicely and the gains exist in her case, but they are not "ground breaking". Any gains are typically mostly due to the compiler's inability to inline the calls rather then the indirection via the vtable. So always measure, please.

Hope that helps. If I have misunderstood what you're trying to do, please come back in the comments.

\$\endgroup\$
3
  • \$\begingroup\$ Heads up: the question contains hypothetical code, which is off-topic for the site, but unfortunately wasn't closed promptly to prevent otherwise legitimate answers like yours, so we may allow the OP to edit their question to fix the problem, possibly invalidating your answer. You may have to rework some parts of your answer. And that's part of the reason we discourage answers to off-topic questions. In the future, please try not to answer off-topic questions and wait for them to be fixed instead. \$\endgroup\$
    – L. F.
    Jan 31, 2020 at 3:32
  • \$\begingroup\$ I see your point, but unfortunately this isn't the correct place to discuss site policies, so I can't discuss with you in depth here. Plus, you have to reach consensus with other members of the community. You can take a look at our FAQ on this, which states the reasons many users think that this rule is necessary, as well as other related questions on Code Review Meta. If you think the rule is outdated or needs improvement, feel free to discuss on Code Review Meta, which is visited by many of the active members, with the arguments you just listed. \$\endgroup\$
    – L. F.
    Jan 31, 2020 at 4:27
  • 1
    \$\begingroup\$ very helpful! thanks very much Oliver \$\endgroup\$
    – Taylor
    Jan 31, 2020 at 15:32

Not the answer you're looking for? Browse other questions tagged or ask your own question.