什么是CRTP?

2023/03/13 模板元编程 共 2119 字,约 7 分钟

CRTP来源

Curiously Recurring Template Pattern ,中文译为奇异的递归模板模式,是由James O. Coplien在其1995年的论文中首次提出。维基百科给出了以下常用模板

// The Curiously Recurring Template Pattern (CRTP)
template <class T>
class Base
{
    // methods within Base can use template to access members of Derived
};
class Derived : public Base<Derived>
{
    // ...
};

这种技术被大量使用在 ATLWTL中,我们平常所熟悉的 enable_shared_from_this也利用了这种技术

class A: public std::enable_shared_from_this<A> {
  // ...
};

CRTP使用

上面展现了一般CRTP的形式,但是这样的形式有什么用呢?

#include <iostream>
template <typename T>
class Base {
public:
    void callDerived() {
        static_cast<T&>(*this).func();
        // static_cast<T*>(this)->func();
    };
};

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

int main() {
  Base<Derived> b;
  b.callDerived();
  return 0;
}

代码看起来有点迷惑,似乎是基类强制转换成派生类,然后就调用了派生类的方法?显然按照下面程序的输出确实是这样的。

Derived::func

How it works

为什么这样的方式能够工作?首先C++普通成员函数的调用在编译后可以理解成通过指针调用的,意味着上面的func会类似下面的形式

void func(Derived* d);

但是强制转换是只能向上转换,即把派生类转换成基类,如果基类转换成派生类导致编译失败,但是通过模板则可以避开这个问题,在上面的例子中,static_cast不会检测模板类型T。因此在经过强制转换之后,就可以调用派生类的方法了,只要不访问派生类的新增数据成员,就不会出现问题。

综上,我们得到了基类访问派生类新增成员函数的方法,类似于 dynamic_cast,但是编译期完成的,编译期多态,性能更高。

局限性

CRTP也有局限性,问题在于Base类实际上是一个模板类,而不是一个实际的类。因此,如果存在名为Derived1Derived12的派生类,则基类模板初始化将具有不同的类型。这是因为,Base类将进行不同类型的特化,代码如下:

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

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

如果创建Base类模板的指针,则意味着存在两种类型的Base指针,即:

// CRTP
Base<Derived1> *b1 = new Derived2;
Base<Derived2> *b2 = new Derived2;

因此不能存储在vector这样的容器中。

还有一个问题是在多个派生类的时候,如果我们不小心写错,可能会出现未定义的问题:

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

class Derived2 : public Base<Derived1> //  Derived1 应该是 Derived2
{
    ...
};

上面这段代码,由于疏忽,写错了,但编译能通过,如何在编译期让这种错误显露出来?

把构造函数私有化即可,这样除非是友元类,否则是不能继承Base的。如果我们写错了,Derived2 就不会成为友元类,就无法通过编译期。

template <typename T>
class Base
{
public:
    // ...
private:
    Base(){};
    friend T;
};

Reference

The Curiously Recurring Template Pattern (CRTP)

What the Curiously Recurring Template Pattern can bring to your code

An Implementation Helper For The Curiously Recurring Template Pattern )

Curiously recurring template pattern

Search

    Table of Contents