(1) The topmost class in the hierarchy looks like:
template <typename T>
class A {
public:
void bar() const {
// do something and then call foo (possibly) in the derived class:
foo();
}
void foo() const {
static_cast<const T*>(this)->foo();
}
protected:
~A() = default;
// Constructors should be protected as well.
};
A<T>::foo()
behaves similarly to a pure virtual method in the sense that it doesn't have a "default implementation" and calls are directed to derived classes. However, this doesn't prevent A<T>
from being instantiated as a non base class. To get this behavior A<T>::~A()
is made protected
.
Remark: Unfortunately a GCC bug turns special member functions public when = default;
is used. In this case, one should used
protected:
~A() {}
Still, protecting the destructor is not enough for the cases where a call to a constructor is not matched by a call to the destructor (this might happen via operator new
). Hence, it's advisable to protect all constructors (including copy- and move-constructor) as well.
When instantiations of A<T>
should be allowed and A<T>::foo()
should behave like a non-pure virtual method, then A
should be similar to the template class B
below.
(2) Classes in the middle of the hierarchy (or the topmost one, as explained in the paragraph above) look like:
template <typename T = void>
class B : public A<B<T>> { // no inherinace if this is the topmost class
public:
// Constructors and destructor
// boilerplate code :-(
void foo() const {
foo_impl(std::is_same<T, void>{});
}
private:
void foo_impl(std::true_type) const {
std::cout << "B::foo()
";
}
// boilerplate code :-(
void foo_impl(std::false_type) const {
if (&B::foo == &T::foo)
foo_impl(std::true_type{});
else
static_cast<const T*>(this)->foo();
}
};
Constructors and destructors are public and T
defaults to void
. This allows objects of type B<>
to be the most derived in the hierarchy and makes this legal:
B<> b;
b.foo();
Notice also that B<T>::foo()
behaves as a non pure virtual method in the sense that, if B<T>
is the most derived class (or, more precisely, if T
is void
), then b.foo();
calls the "default implementation of foo()
" (which outputs B::foo()
). If T
is not void
, then the call is directed to the derived class. This is accomplished through tag dispatching.
The test &B::foo == &T::foo
is required to avoid an infinite recursive call. Indeed, if the derived class, T
, doesn't reimplement foo()
, the call static_cast<const T*>(this)->foo();
will resolve to B::foo()
which calls B::foo_impl(std::false_type)
again. Furthermore, this test can be resolved at compile time and the code is either if (true)
or if (false)
and the optimizer can remove the test altogether (e.g. GCC with -O3).
(3) Finally, the bottom of the hierarchy looks like:
class C : public B<C> {
public:
void foo() const {
std::cout << "C::foo()
";
}
};
Alternatively, one can remove C::foo()
entirely if the inherited implementation (B<C>::foo()
) is adequate.
Notice that C::foo()
is similar to a final method in the sense that calling it does not redirected the call to a derived class (if any). (To make it non final, a template class like B
should be used.)
(4) See also:
How to avoid errors while using CRTP?