C++ 中的 **CRTP
CRTP奇特重现模板模式
它的范式基本上都是:父类是模板,再定义一个类类型去继承它。因为类模板不是类,只有实例化后的类模板才是实际的类类型,所以我们需要实例化它,显示指明模板类型参数,而这个参数就是我们定义的类型,也就是子类了。
template<class Dervied>
class Base{};
//类模板
class x : public Base<x> {};//可以传自己x,也可以传别的,比如Int
CRTP可用于在父类暴露接口,而子类实现该接口,以此实现“编译器多态”,或者“静态多态”
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include<type_traits>
template <class Dervied>
class Base {
public:
//公开的接口函数,供外部调用
void addWater(int n)
{
//调用子类的实现函数,要求子类必须实现名为impl()的函数
//这个函数会被调用来执行具体的操作
static_cast<Dervied*>(this)->impl(n);
//很明显进行了类型转换,将this指针(也就是父类的指针),转换为通过模板参数传递的类型,
//也就是子类的指针。这个转换时安全合法的。因为this指针实际上指向一个X类型的对象,
//X类型对象继承了Base<x>的部分,x对象也就包含了Base<x>的部分,
//所以这个转换在编译期是有效的并且合法的
//当你调用了x.addwater()时,实际上是x对象调用了父类Base<X>的成员函数。这个成员函数内部使用了
//static_cast<Dervied*>(this)->impl(n);将this从Base<x>* 转换为x*,然后调用x中的impl()函数
}
};
class X :public Base<X> {
public:
void impl(int n) const {
std::cout << "X设备加了" << n << "毫升水\n";
}
};
class Y :public Base<Y> {
public:
void impl(int n) const {
std::cout << "Y设备加了" << n << "毫升水\n";
}
};
int main()
{
X x;
x.addWater(100);
Y y;
y.addWater(50);
}
C++ 中的 **CRTP(Curiously Recurring Template Pattern,奇异递归模板模式)** 是一种通过模板继承实现**静态多态**的技术。它的核心思想是让基类模板以派生类作为模板参数,从而在编译期实现类似虚函数的多态行为,但避免了运行时的性能开销。以下从多个角度详细解释:
---
### 一、CRTP 的基本结构与工作原理
#### 1. 核心代码结构
```cpp
// 基类模板,以派生类作为模板参数
template <typename Derived>
class Base {
public:
void interface() {
// 通过静态转换调用派生类的实现
static_cast<Derived*>(this)->implementation();
}
};
// 派生类继承基类,并将自身作为模板参数传递给基类
class Derived : public Base<Derived> {
public:
void implementation() {
std::cout << "Derived 的具体实现" << std::endl;
}
};
```
• **关键点**:
• 基类 `Base` 是一个模板类,其模板参数是派生类 `Derived`。
• 基类通过 `static_cast<Derived*>(this)` 将自身指针转换为派生类指针,从而调用派生类的方法。
#### 2. 静态多态的实现
• **传统虚函数**:通过虚函数表(vtable)在运行时动态绑定,产生性能开销。
• **CRTP**:在编译期完成类型绑定,直接调用派生类的实现,没有运行时开销。
```cpp
Derived d;
d.interface(); // 编译期确定调用 Derived::implementation()
```
---
### 二、CRTP 的优势与典型应用场景
#### 1. 性能优势
• **避免虚函数开销**:虚函数需要查表(vtable)和间接调用,CRTP 直接编译期绑定,适合高频调用的场景(如数学库、游戏引擎)。
• **示例**:向量运算中,基类通过 CRTP 调用派生类的运算符重载,实现高效的静态多态。
#### 2. 应用场景
##### (1) 静态多态(替代虚函数)
```cpp
template <typename Derived>
class Logger {
public:
void log(const std::string& message) {
static_cast<Derived*>(this)->writeLog(message);
}
};
class ConsoleLogger : public Logger<ConsoleLogger> {
public:
void writeLog(const std::string& msg) {
std::cout << "[Console] " << msg << std::endl;
}
};
```
• 通过 `Logger` 基类统一接口,不同派生类实现具体的日志写入逻辑。
##### (2) 对象计数器
```cpp
template <typename T>
class Counter {
static int count; // 每个派生类有独立的计数器
public:
Counter() { count++; }
~Counter() { count--; }
static int getCount() { return count; }
};
class A : public Counter<A> {}; // A 的计数器
class B : public Counter<B> {}; // B 的计数器
```
• 每个派生类维护独立的静态变量,避免传统继承中所有子类共享同一计数的问题。
##### (3) 标准库中的应用
• **`std::enable_shared_from_this`**:允许对象安全地返回自身的 `shared_ptr`,其实现依赖 CRTP:
```cpp
class MyClass : public std::enable_shared_from_this<MyClass> {
public:
std::shared_ptr<MyClass> getShared() {
return shared_from_this(); // 通过基类方法获取智能指针
}
};
```
---
### 三、CRTP 的注意事项与局限性
#### 1. 注意事项
• **模板参数必须正确**:若派生类错误传递其他类型(如 `class Derived2 : public Base<Derived1>`),会导致未定义行为。
• 可通过将基类构造函数设为私有,并声明派生类为友元来避免此问题:
```cpp
template <typename T>
class Base {
private:
Base() {}
friend T; // 仅允许派生类访问构造函数
};
```
• **名称遮掩(Name Hiding)**:若派生类定义了与基类同名的方法,会覆盖基类方法。需使用 `using` 显式引入基类方法。
#### 2. 局限性
• **编译期绑定**:无法在运行时动态切换派生类类型(如工厂模式中动态创建对象)。
• **代码膨胀**:每个派生类会生成独立的基类模板实例,可能增加代码体积,但现代编译器可优化此问题。
---
### 四、CRTP 与动态多态的对比
| **特性** | **CRTP(静态多态)** | **动态多态(虚函数)** |
|----------------|--------------------------|---------------------------|
| **性能** | 无运行时开销 | 有虚函数表查找开销 |
| **灵活性** | 编译期绑定,类型固定 | 运行时动态绑定,类型可变 |
| **代码复杂度** | 模板语法复杂,可读性较低 | 直观,但虚函数需继承体系 |
| **适用场景** | 高频调用、性能敏感场景 | 需运行时类型切换的场景 |
---
### 五、总结
CRTP 通过模板继承的“递归”特性,在编译期实现了高效的多态行为。**核心价值**包括:
1. **性能优化**:避免虚函数开销,适合数学运算、高频调用逻辑。
2. **代码复用**:通过基类模板统一接口,派生类灵活实现细节(如策略模式)。
3. **类型安全**:编译期检查派生类与基类的关联,减少运行时错误。
**学习建议**:从简单的计数器、日志器入手实践,再逐步探索复杂场景(如自定义数据结构优化)。