CRTP 是一个非常经典的C++静态多态实现方案,在很多库中都有应用,这里整理一下。

概述

奇异递归模板模式(Curiously Recurring Template Pattern, CRTP)是 C++ 编程中一种常用的静态多态方案,它通过模板继承和静态绑定来实现类型多态。 相比于C++直接提供的基于虚函数的动态多态方案,CRTP既可以避免虚函数的额外性能开销,还可以在编译时获得更好的类型检查和优化。

CRTP的核心思想如下:

  • 模板基类是一个模板类,模板参数是派生类自身。(这种递归的模板参数是这个模式名字的来源)
  • 派生类:派生类继承自通过自身类型实例化的基类

通过基类调用方法时,首先会将this指针进行类型转换为派生类的类型(因为派生类类型是模板参数,基类可以获取到派生类类型),然后就可以调用派生类的方法。 这种处理导致我们无法添加基类自身的方法实现并使用,因此基类只能作为一个抽象基类使用。 这一切都发生在模板实例化的过程中,在编译期中即可完全确定。

CRTP具有如下的优势:

  • 静态多态:CRTP的是静态多态,通过基类调用派生类的方法是静态绑定的,完全在编译期通过模板实例化确定。与动态多态相比效率更高,因为完全避免了虚函数调用的开销。
  • 类型安全:CRTP在编译时可以进行类型检查,从而确保方法调用的类型安全。

事实上,除了原理不同,CRTP和虚函数的使用情景并不是完全重合的,例如CRTP可以使用在某些虚函数无法应用的情景(静态成员函数,模板成员函数等)。

示例

我们从传统的,基于虚函数的动态多态出发,有下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
#include <string>

class Base {
public:
virtual ~Base() = default;

virtual void run() const { std::cout << "Base Run" << std::endl; }
};

class Derived1 : public Base {
private:
std::string m_name;

public:
explicit Derived1(std::string name) : m_name(std::move(name)) {}

void run() const override {
std::cout << "Derived1 Run, his name is " << m_name << std::endl;
}
};

class Derived2 : public Base {
private:
std::string m_name;

public:
explicit Derived2(std::string name) : m_name(std::move(name)) {}

void run() const override {
std::cout << "Derived2 Run, her name is " << m_name << std::endl;
}
};

void Action(Base &obj) { obj.run(); }

int main() {
Base b;
Action(b);

Derived1 d1("Tom");
Action(d1);

Derived2 d2("Jerry");
Action(d2);

return 0;
}

运行结果如下

1
2
3
Base Run
Derived1 Run, his name is Tom
Derived2 Run, her name is Jerry

其中的Action函数中,我们通过基类的引用(或指针)调用了虚函数,实现了动态多态

1
2
3
void Action(Base &obj) {
obj.run();
}

在CRTP的实现中,我们将派生类作为模板参数传递给基类,通过基类调用时,首先通过类型转换到派生类,然后调用对应的方法。

1
2
3
4
5
template <typename Derived>
class Base {
public:
void run() { static_cast<Derived *>(this)->run(); }
};

CRTP的完整实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
#include <string>

template <typename Derived>
class Base {
public:
void run() { static_cast<Derived *>(this)->run(); }
};

class Derived1 : public Base<Derived1> {
private:
std::string m_name;

public:
explicit Derived1(std::string name) : m_name(std::move(name)) {}

void run() const {
std::cout << "Derived1 Run, his name is " << m_name << std::endl;
}
};

class Derived2 : public Base<Derived2> {
private:
std::string m_name;

public:
explicit Derived2(std::string name) : m_name(std::move(name)) {}

void run() const {
std::cout << "Derived2 Run, her name is " << m_name << std::endl;
}
};

template <typename T>
void Action(Base<T> &obj) {
obj.run();
}

int main() {
Derived1 d1("Tom");
Action(d1);

Derived2 d2("Jerry");
Action(d2);
return 0;
}

运行结果如下

1
2
Derived1 Run, his name is Tom
Derived2 Run, her name is Jerry

注意到这里我们无法实例化和调用基类对象,基类只是起到抽象基类的作用。