CRTP:奇异递归模板模式的原理与应用

1.5k words

C++ CRTP 详解:奇异递归模板模式的原理与应用

在 C++ 编程中,模板不仅仅用于泛型编程,还可以发挥出“元编程”的威力。CRTP,即“奇异递归模板模式”,正是一种利用模板实现静态多态和代码复用的设计模式。本文将从原理、示例、优缺点以及新标准对其的改进等多个角度详细讲解 CRTP。


CRTP 简介

CRTP 的英文全称为 _Curiously Recurring Template Pattern_,顾名思义,就是让一个类在继承时将自身作为模板参数传给其基类。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 基类模板
template <typename Derived>
class Base {
public:
// 调用派生类的成员函数 implementation()
void interface() {
static_cast<Derived*>(this)->implementation();
}
// 如果派生类没有重写,基类可以提供一个默认实现
void implementation() {
// 默认行为
}
};

// 派生类,将自身作为模板参数传入基类
class Derived1 : public Base<Derived1> {
public:
void implementation() {
std::cout << "Derived1::implementation()" << std::endl;
}
};

上述代码中,Derived1 继承自 Base<Derived1>,基类中通过 static_castthis 转换为派生类指针,从而在调用 interface() 时能够调用到派生类重写后的 implementation() 方法。这种技术利用了 C++ 模板延迟实例化的特性,可以在编译期实现类似于多态的效果,而无需虚函数的运行时开销

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
#include <iostream>
using namespace std;

template <typename Derived>
class Shape {
public:
// 定义统一接口,调用派生类实现的 do_draw()
void draw() {
static_cast<Derived*>(this)->do_draw();
}
};

class Circle : public Shape<Circle> {
public:
void do_draw() {
cout << "Drawing a circle" << endl;
}
};

class Square : public Shape<Square> {
public:
void do_draw() {
cout << "Drawing a square" << endl;
}
};

int main() {
Circle c;
Square s;
c.draw(); // 输出 "Drawing a circle"
s.draw(); // 输出 "Drawing a square"
return 0;
}

在这个例子中,Shape 模板类定义了一个 draw() 方法,该方法内部将 this 强制转换为派生类指针,从而调用派生类的 do_draw()。这样就实现了在编译期确定调用函数的目标,避免了虚函数表查找

Mixin 与代码复用

CRTP 也常用于 mixin 风格的设计,即在不使用虚函数的情况下为类添加额外功能。例如,我们可以通过 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
#include <iostream>

template <typename T>
class ObjectCounter {
public:
ObjectCounter() { ++objects_created; ++objects_alive; }
ObjectCounter(const ObjectCounter&) { ++objects_created; ++objects_alive; }
protected:
~ObjectCounter() { --objects_alive; }
public:
static int getObjectsCreated() { return objects_created; }
static int getObjectsAlive() { return objects_alive; }
private:
static int objects_created;
static int objects_alive;
};

template <typename T>
int ObjectCounter<T>::objects_created = 0;

template <typename T>
int ObjectCounter<T>::objects_alive = 0;

class MyClass : public ObjectCounter<MyClass> {
// 其它成员
};

int main() {
MyClass a, b;
std::cout << "MyClass created: " << MyClass::getObjectsCreated() << std::endl;
std::cout << "MyClass alive: " << MyClass::getObjectsAlive() << std::endl;
return 0;
}

这里,每个派生类都通过独立实例化的 ObjectCounter<T> 来统计自己对象的数量,因为模板参数不同(如 MyClass 与其他类)生成的是不同的基类类型

链式调用实现

在实际项目中,链式调用(fluent interface)是一种常见的设计风格,但如果基类方法返回的是基类引用,继承的派生类特有方法就无法连贯调用。利用 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
#include <iostream>
#include <ostream>

template <typename Derived>
class Printer {
public:
explicit Printer(std::ostream& os) : stream(os) {}

template <typename T>
Derived& print(const T& data) {
stream << data;
return static_cast<Derived&>(*this);
}

template <typename T>
Derived& println(const T& data) {
stream << data << std::endl;
return static_cast<Derived&>(*this);
}

private:
std::ostream& stream;
};

class CoutPrinter : public Printer<CoutPrinter> {
public:
CoutPrinter() : Printer(std::cout) {}
// 增加派生类特有的接口
CoutPrinter& setColor(const std::string& color) {
// 设定控制台颜色(示例)
std::cout << "[Color:" << color << "]";
return *this;
}
};

int main() {
CoutPrinter().print("Hello ").setColor("red").println("World!");
return 0;
}

通过 CRTP,使得 print()println() 返回的是 CoutPrinter& 而非基类引用,从而可以连续调用 setColor()


CRTP 与动态多态的比较

在传统的动态多态中,我们通常使用虚函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
class Base {
public:
virtual void func() = 0;
virtual ~Base() = default;
};

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

这种方式的优点是可以将不同派生类统一存放在一个基类指针容器中,但缺点在于每个对象需要额外存储虚表指针,且每次调用都需要通过虚函数表查找,存在一定性能开销。CRTP 则通过编译期静态绑定实现多态,消除了运行时开销,但牺牲了“同一基类指针统一管理”的能力,因为不同模板参数实例化出的基类类型是不同的

CRTP 的优势与局限

优势

  1. 高效性
    CRTP 完全在编译期解析,不产生虚函数调用的开销,有利于内联和编译器优化。

  2. 灵活性
    利用模板参数可以让基类访问派生类的成员、方法,方便实现接口默认实现和代码复用。

  3. 零运行时成本
    不需要虚表指针、动态绑定,适用于性能敏感场景。

局限

  1. 代码膨胀
    每个派生类实例化出一个独立的基类模板可能导致生成代码增多。

  2. 不可统一管理
    由于基类模板实例化类型不同,不便将不同派生类对象存放于同一容器中,需要借助额外的抽象层(例如虚基类)来解决。

  3. 错误提示复杂
    模板错误消息往往较为晦涩,不易调试。

  4. 设计要求较高
    需要保证派生类在继承时满足基类所要求的接口,否则静态转换(static_cast)可能出错。

Comments