[C + +] CRTP: singular recursive template mode

 

1. What is CRTP?

What is CRTP? The full name of CRTP is curiously recursive template pattern, that is, singular recursive template pattern, or CRTP for short. CRTP is a special template technology and usage, which is a common usage in C + + template programming. The characteristics of CRTP are as follows:

  1. The base class is a template class
  2. When a derived class inherits the base class, it passes the derived class itself as a template parameter to the base class

Typical codes are as follows:

// The base class is a template class
template <typename T>
class Base
{
public:
    virtual ~Base() {}

    void func()
    {
        if (auto t = static_cast<T *>(this))
        {
            t->op();
        }
    }
};

// The Derived class inherits from Base and passes itself as a template parameter to the Base class
class Derived : public Base<Derived>
{
public:
    void op()
    {
        std::cout << "Derived::op()" << std::endl;
    }
};

As you can see, inside the base class, by using static_cast, transform the this pointer to the pointer of the template parameter type T, then call the type T method. Here's a question:

static_ Is cast conversion safe?

We know that when static_cast is used to convert the pointer or reference between the base class (parent class) and the derived class (child class) in the class hierarchy. It is safe to conduct uplink conversion (convert the pointer or reference of the derived class into the representation of the base class); Downstream conversion (converting a base class pointer or reference to a derived class representation) is not necessarily safe because there is no dynamic type checking.

However, the design principle of CRTP is to assume that Derived will inherit from Base. CRTP requires that all Derived classes should be defined in the following form:

class Derived1 : public Base<Derived1> {};
class Derived2 : public Base<Derived2> {};

From the perspective of Base class objects, Derived class objects are themselves (that is, Derived is a Base and cat is an animal).

In actual use, we only use Derived1 and Derived2 objects, and will not directly use base < Derived1 > and base < Derived2 > types to define objects. This ensures that when static_ When cast is executed, the pointer of the base class base < DerivedX > must point to the object of a subclass DerivedX, so the conversion is safe.

Characteristics of CRTP

  • Advantages: it eliminates the overhead caused by dynamic binding and querying virtual function table. Through CRTP, the base class can obtain the types of derived classes and provide various operations, which is more flexible than ordinary inheritance. However, the CRTP base class will not be used alone, but as a template.
  • Disadvantages: the common problem of the template is that it affects the readability of the code.

2. Purpose of CRTP

2.1. Static distribution ("static polymorphism")

Polymorphism means that the same method has different behaviors between the base class and different derived classes. However, the base class inherited by each derived class in CRTP varies with the template parameters, that is, base < derived1 > and base < derived2 > are not the same base class type. Therefore, the "static polymorphism" here is quoted, indicating that it is not a polymorphism in the strict sense.

template <typename T>
class Base
{
public:
    Base() {}
    virtual ~Base() {}

    void func()
    {
        if (auto t = static_cast<T *>(this))
        {
            t->op();
        }
    }
};

class Derived1 : public Base<Derived1>
{
public:
    Derived1() {}
    void op()
    {
        std::cout << "Derived1::op()" << std::endl;
    }
};

class Derived2 : public Base<Derived2>
{
public:
    Derived2() {}
    void op()
    {
        std::cout << "Derived2::op()" << std::endl;
    }
};

// Auxiliary function: complete static distribution
template<typename DerivedClass>
void helperFunc(Base<DerivedClass>& d)
{
    d.func();
}

int main(int argc, char* argv[]) 
{
    Derived1 d1;
    Derived2 d2;
    helperFunc(d1);
    helperFunc(d2);

    return 0;
}

The following is the output of the code:

Derived1::op()
Derived2::op()

Template classes or template functions are instantiated only when called. Therefore, when base < derived1 >:: func() is called, base < derived1 > already knows the existence of Derived1::op().

2.2. Counter

Implement counters of different subclasses:

template<typename T>
class Counter
{
public:
    static int count;
    Counter()
    {
        ++Counter<T>::count;
    }
    ~Counter() 
    {
        --Counter<T>::count;
    }
};
template<typename T>
int Counter<T>::count = 0;

class DogCounter : public Counter<DogCounter>
{
public:
    int getCount()
    {
        return this->count;
    }
};

class CatCounter : public Counter<CatCounter>
{
public:
    int getCount()
    {
        return this->count;
    }
};

int main(int argc, char* argv[]) 
{
    DogCounter d1;
    std::cout << "DogCount : " << d1.getCount() << std::endl;
    {
        DogCounter d2;
        std::cout << "DogCount : " << d1.getCount() << std::endl;
    }
    std::cout << "DogCount : " << d1.getCount() << std::endl;

    CatCounter c1, c2, c3, c4, c5[3];
    std::cout << "CatCount : " << c1.getCount() << std::endl;

    return 0;
}

Run the above code and the output is as follows:

DogCount : 1
DogCount : 2
DogCount : 1
CatCount : 7

But what's the use of counters? Will we define and use counters like this in our code? I don't know. At least here, we can deepen our understanding of CRTP.

3. Application of CRTP in the project

CRTP is widely used in open source projects.

3.1. LLVM/MLIR

CRTP technology is widely used in LLVM, and a random code is intercepted as follows:

namespace mlir {
class Operation final
    : public llvm::ilist_node_with_parent<Operation, Block>,
      private llvm::TrailingObjects<Operation, BlockOperand, Region,
                                    detail::OperandStorage> {
public:
    /// ...

At the declaration of mlir::Operation, one of the most important data structures in MLIR, you can see that it inherits from a template base class. We transfer to this base class:

template <typename NodeTy, typename ParentTy, class... Options>
class ilist_node_with_parent : public ilist_node<NodeTy, Options...> {
protected:
  ilist_node_with_parent() = default;

private:
  /// Forward to NodeTy::getParent().
  ///
  /// Note: do not use the name "getParent()".  We want a compile error
  /// (instead of recursion) when the subclass fails to implement \a
  /// getParent().
  const ParentTy *getNodeParent() const {
    return static_cast<const NodeTy *>(this)->getParent();
  }

You can see that static also exists in the getNodeParent() interface_ cast. There are countless such uses in the whole open source project.

3.2. enable_shared_from_this

When a class wants to return this in the smart pointer version, it needs to inherit enable_shared_from_this, via shared_from_this() returns the corresponding smart pointer.

	// CLASS TEMPLATE enable_shared_from_this
template<class _Ty>
	class enable_shared_from_this
	{	// provide member functions that create shared_ptr to this
public:
	using _Esft_type = enable_shared_from_this;

	_NODISCARD shared_ptr<_Ty> shared_from_this()
		{	// return shared_ptr
		return (shared_ptr<_Ty>(_Wptr));
		}

	_NODISCARD shared_ptr<const _Ty> shared_from_this() const
		{	// return shared_ptr
		return (shared_ptr<const _Ty>(_Wptr));
		}

	_NODISCARD weak_ptr<_Ty> weak_from_this() noexcept
		{	// return weak_ptr
		return (_Wptr);
		}

	_NODISCARD weak_ptr<const _Ty> weak_from_this() const noexcept
		{	// return weak_ptr
		return (_Wptr);
		}

Keywords: C++

Added by jipacek on Sat, 26 Feb 2022 06:18:31 +0200