C++中的多态通常依赖虚函数实现,这种方式会在运行时通过虚函数表查找调用对应函数,存在一定的性能开销。而CRTP(Curiously Recurring Template Pattern,奇异递归模板模式)可以在编译期实现多态效果,完全不需要虚函数的参与,是C++模板编程中非常实用的进阶技巧。

CRTP的核心原理
CRTP的核心结构是一个模板基类,它的模板参数就是继承自它的派生类。基类通过模板参数知道自己对应的派生类类型,从而可以在编译期直接调用派生类的成员函数,不需要运行时查找。
最基础的CRTP结构如下:
// 模板基类,模板参数为派生类类型
template <typename Derived>
class Base {
public:
void interface() {
// 静态转换到派生类,调用派生类的实现
static_cast<Derived*>(this)->implementation();
}
};
// 派生类继承自Base,并将自身作为模板参数传入
class Derived : public Base<Derived> {
public:
void implementation() {
// 派生类的具体实现逻辑
}
};
这里基类<code>Base</code>的模板参数是<code>Derived</code>,当<code>Derived</code>继承<code>Base<Derived></code>时,基类在编译期就已经知道派生类的具体类型,通过<code>static_cast</code>转换后可以直接调用派生类的<code>implementation</code>函数,整个过程没有虚函数参与,也没有运行时开销。
CRTP实现编译期多态的完整示例
下面通过一个具体的场景来展示CRTP如何实现多态:假设我们有多种不同的图形类型,都需要计算面积和打印信息,但是不同图形的计算逻辑不同。
传统虚函数实现方式
先看看传统虚函数的实现,方便对比:
#include <iostream>
#include <cmath>
// 虚函数基类
class Shape {
public:
virtual double getArea() const = 0;
virtual void printInfo() const = 0;
virtual ~Shape() = default;
};
// 圆形类
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double getArea() const override {
return 3.14159 * radius * radius;
}
void printInfo() const override {
std::cout << "Circle, radius: " << radius << ", area: " << getArea() << std::endl;
}
};
// 矩形类
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double getArea() const override {
return width * height;
}
void printInfo() const override {
std::cout << "Rectangle, width: " << width << ", height: " << height << ", area: " << getArea() << std::endl;
}
};
CRTP实现方式
现在用CRTP重写上面的逻辑:
#include <iostream>
#include <cmath>
// CRTP模板基类
template <typename Derived>
class Shape {
public:
double getArea() const {
// 调用派生类的面积计算实现
return static_cast<const Derived*>(this)->getAreaImpl();
}
void printInfo() const {
// 调用派生类的信息打印实现
static_cast<const Derived*>(this)->printInfoImpl();
}
};
// 圆形类,继承时传入自身作为模板参数
class Circle : public Shape<Circle> {
private:
double radius;
public:
Circle(double r) : radius(r) {}
// 基类会调用的实现函数
double getAreaImpl() const {
return 3.14159 * radius * radius;
}
void printInfoImpl() const {
std::cout << "Circle, radius: " << radius << ", area: " << getAreaImpl() << std::endl;
}
};
// 矩形类,继承时传入自身作为模板参数
class Rectangle : public Shape<Rectangle> {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
// 基类会调用的实现函数
double getAreaImpl() const {
return width * height;
}
void printInfoImpl() const {
std::cout << "Rectangle, width: " << width << ", height: " << height << ", area: " << getAreaImpl() << std::endl;
}
};
// 通用函数,可以接收任意Shape的派生类
template <typename T>
void processShape(const Shape<T>& shape) {
shape.printInfo();
}
使用时和虚函数版本类似,但是不需要虚函数表:
int main() {
Circle c(5.0);
Rectangle r(3.0, 4.0);
processShape(c);
processShape(r);
return 0;
}
CRTP的适用场景与注意事项
适用场景
- 需要多态效果但是对性能要求极高,无法接受虚函数的运行时开销
- 多个类有相似的行为框架,但是具体实现不同,需要避免代码重复
- 编译期就能确定所有类型,不需要运行时的类型动态变化
注意事项
- CRTP是编译期绑定的,无法实现运行时的动态多态,比如不能把不同派生类对象放到同一个基类指针数组中
- 派生类必须正确实现基类期望的函数,否则编译期就会报错,这一点比虚函数的编译检查更严格
- CRTP会增加编译时间,因为模板需要在编译期实例化,复杂场景下可能导致代码膨胀
- 基类的函数如果要访问派生类的成员,必须保证派生类的对应成员在基类调用时已经可见,通常建议把实现函数放在派生类的public区域或者基类可访问的区域
CRTP与其他模板技巧的对比
CRTP和模板特化、concept等C++特性可以结合使用,但是核心优势还是无虚函数的编译期多态。相比虚函数多态,CRTP没有运行时开销,但是灵活性更低;相比普通模板,CRTP提供了更统一的行为框架,减少了重复代码。
在实际项目中,CRTP常被用在标准库、游戏引擎、高性能计算框架中,比如<code>std::enable_shared_from_this</code>就是CRTP的典型应用,它通过CRTP让派生类能够安全地生成自身的<code>shared_ptr</code>对象。