一、前言
C++ 最核心的是支持 泛型编程。用模板 (Templates) 机制实现高度的代码复用。不管是标准库中的容器,还是算法,都离不开模板。
但是,模板是非常 复杂、很难掌握的。特别是冗长的编译错误信息、复杂的模板元编程语法。
一组有共同行为或接口的类,要共享一些功能,但又希望避免传统虚函数带来的运行时开销。如何在追求性能和代码简洁的同时,实现静态多态和通用行为?
奇异递归模板模式 (Curiously Recurring Template Pattern, CRTP) ,精妙的方式解决上述要求。这篇文章就 CRTP 的原理、优势,编写更加高效、简洁、类型安全的 C++ 代码。

二、什么是奇异递归模板模式?
奇异递归模板模式 (Curiously Recurring Template Pattern),简称 CRTP,是 C++ 模板编程一种独特的技术。
CRTP 的简单理解:一个基类模板把派生类作为模板参数。
听起来是不是有点奇怪?基类不应该是独立于派生类存在的吗?但 CRTP 不走寻常路,CRTP 是一种递归的模板参数化方式,可以让基类在编译时就知道派生类的具体类型。
CRTP 的基本形式:
template <typename Derived>
class Base {
// 基类模板的实现
// Derived& getDerived() { return static_cast<Derived&>(*this); }
};
class Derived : public Base<Derived> {
// 派生类的实现
// 继承自 Base<Derived>,把自身作为基类的模板参数
};
CRTP 之所以被称为“奇异递归”,就是因为派生类 Derived 在定义完成之前,就已经作为模板参数传递给它的基类 Base。在逻辑上好像形成一个循环依赖:Derived 要 Base<Derived> 才能定义, Base<Derived> 又要 Derived 的定义。
C++ 编译器能处理这种“前向声明”式的依赖。编译器处理 Base<Derived> 时知道 Derived 是一个类,不会检查 Derived 的完整定义。只要 Derived 的完整定义可用,Base<Derived> 就可以用 Derived 的所有信息。基类模板可以在编译时用 static_cast 把 Base* 或 Base& 转换为 Derived* 或 Derived&,从而调用派生类特有的成员函数或访问其成员。
CRTP 的核心思想是用编译期信息实现静态多态和代码生成。把派生类类型注入到基类模板,基类就可以在编译时获得关于派生类的类型信息,从而:
- 在基类定义通用接口或行为,派生类只用实现特定部分。
- 强制派生类实现某些特定方法,否则会导致编译错误。编译时就检查错误,不用等到运行时.
- 在编译时确定调用哪个函数,避免虚函数表的运行时查找开销。
- 实现 Mixin 类和策略模式
三、CRTP 实现通用的比较操作符
C++ 经常要为自定义的类实现各种比较操作符:== (相等)、!= (不等)、< (小于) 等。
常见的模式:如果已经实现 operator==,那么 operator!= 就可以简单定义为 !(a == b)。同样,如果实现了 operator<,那么 operator>、operator<=、operator>= 都可以基于 operator< 和 operator== 来实现。
但是,为每一个要比较的类手动编写这些重复的 operator!=、operator> 等操作符,会产生大量的样板代码。还是希望用一种机制,自动生成这些通用的比较操作符。
CRTP 就可以解决这个问题。
设计一个 Comparable 基类模板,接收派生类作为模板参数。Comparable 基类负责提供所有通用的比较操作符,派生类只要实现一个核心的、特有的比较逻辑。
示例:
#include <iostream>
#include <string>
#include <utility>
// CRTP 基类:为派生类提供通用的比较操作符
template <typename Derived>
class Comparable {
public:
// operator== 的实现。
bool operator==(const Derived& other) const {
// static_cast<const Derived*>(this) 把当前对象的基类指针转换为派生类指针
// 然后调用 Derived 类的 equalsImpl 方法
return static_cast<const Derived*>(this)->equalsImpl(other);
}
// operator!= 的实现。
// 基于 operator== 自动生成,不用派生类重复编写。
bool operator!=(const Derived& other) const {
return !(*static_cast<const Derived*>(this) == other);
}
// ---------------------------------------------------------------------
// 支持小于、大于等操作符
// 派生类只要实现 lessImpl 方法。
bool operator<(const Derived& other) const {
return static_cast<const Derived*>(this)->lessImpl(other);
}
bool operator>(const Derived& other) const {
return other < *static_cast<const Derived*>(this); // 用 operator< 实现 operator>
}
bool operator<=(const Derived& other) const {
return !(*static_cast<const Derived*>(this) > other); // 用 operator> 实现 operator<=
}
bool operator>=(const Derived& other) const {
return !(*static_cast<const Derived*>(this) < other); // 用 operator< 实现 operator>=
}
// ---------------------------------------------------------------------
};
// 派生类 1:Point
// Point 类继承自 Comparable<Point>。
class Point : public Comparable<Point> {
public:
Point(int x_val, int y_val) : x(x_val), y(y_val) {}
// Point 类特有的相等比较实现。
// 必须命名为 equalsImpl,因为 Comparable 基类会调用它。
bool equalsImpl(const Point& other) const {
return x == other.x && y == other.y;
}
void print() const {
std::cout << "Point(" << x << ", " << y << ")";
}
protected:
int x, y;
};
// 派生类 2:Person
// 同样继承自 Comparable<Person>。
class Person : public Comparable<Person> {
public:
Person(std::string name_val, int age_val) : name(std::move(name_val)), age(age_val) {}
// Person 类特有的相等比较实现。
bool equalsImpl(const Person& other) const {
return name == other.name && age == other.age;
}
void print() const {
std::cout << "Person(Name: " << name << ", Age: " << age << ")";
}
protected:
std::string name;
int age;
};
// 派生类没有实现 equalsImpl,会发生编译错误!
// class BadClass : Comparable<BadClass> {};
int main()
{
std::cout << "--- Point 对象的比较 ---" << std::endl;
Point p1(1, 2);
Point p2(1, 2);
Point p3(3, 4);
std::cout << "p1: "; p1.print(); std::cout << std::endl;
std::cout << "p2: "; p2.print(); std::cout << std::endl;
std::cout << "p3: "; p3.print(); std::cout << std::endl;
std::cout << "p1 == p2: " << (p1 == p2 ? "true" : "false") << std::endl; // true
std::cout << "p1 != p3: " << (p1 != p3 ? "true" : "false") << std::endl; // true
std::cout << "p1 == p3: " << (p1 == p3 ? "true" : "false") << std::endl; // false
std::cout << "\n--- Person 对象的比较 ---" << std::endl;
Person person1("Alice", 30);
Person person2("Alice", 30);
Person person3("Bob", 25);
std::cout << "person1: "; person1.print(); std::cout << std::endl;
std::cout << "person2: "; person2.print(); std::cout << std::endl;
std::cout << "person3: "; person3.print(); std::cout << std::endl;
std::cout << "person1 == person2: " << (person1 == person2 ? "true" : "false") << std::endl; // true
std::cout << "person1 != person3: " << (person1 != person3 ? "true" : "false") << std::endl; // true
// BadClass b1, b2;
// std::cout << (b1 == b2) << std::endl;
return 0;
}
输出:
--- Point 对象的比较 ---
p1: Point(1, 2)
p2: Point(1, 2)
p3: Point(3, 4)
p1 == p2: true
p1 != p3: true
p1 == p3: false
--- Person 对象的比较 ---
person1: Person(Name: Alice, Age: 30)
person2: Person(Name: Alice, Age: 30)
person3: Person(Name: Bob, Age: 25)
person1 == person2: true
person1 != person3: true
这个 CRTP 模式在 C++ 的 Boost 库使用特别多。
四、CRTP 的其他应用场景
(1)Mixin 类:为类注入特定功能。
Mixin 是一种设计模式,可以在不用多重继承的情况下,把一组特定的功能(方法和/或数据)“混合”到另一个类。CRTP 是实现 Mixin 的非常好的方式。
实现方式: 创建一个 CRTP 基类模板,包含要注入的功能。派生类继承这个 CRTP 基类获得这些功能。因为基类模板知道派生类的类型,可以安全调用派生类的方法或访问其成员,甚至在注入的功能中引用派生类自身。
典型应用:
- 计数器: 为所有派生类自动添加对象计数功能(构造多少个,销毁多少个)。
- 日志记录: 注入统一的日志接口,让派生类方便记录事件。
- 单例模式: Mixin 强制派生类实现单例行为。
- 观察者模式: Mixin 实现注册/注销观察者和通知观察者的通用逻辑。
示例(计数器 Mixin):
template <typename Derived>
class Counted {
public:
Counted() { ++s_count; }
~Counted() { --s_count; }
static int getCount() { return s_count; }
private:
static int s_count;
};
template <typename Derived>
int Counted<Derived>::s_count = 0;
class MyClass : public Counted<MyClass> {
// MyClass 自动获得对象计数功能
};
(2)策略模式:用模板参数定制算法或行为。
策略模式在运行时选择算法的行为。基于策略的模板设计把这种选择推到编译时,用模板参数定制类的行为。CRTP 可以把策略类“注入”到主体类,让主体类用 static_cast 调用策略类的方法。
实现方式: 主体类是一个模板,模板参数之一是策略类。如果策略类要访问主体类的内部状态或方法,CRTP 可以让策略类反向访问主体类。
典型应用:
- 内存管理策略: 一个容器可以根据模板参数选择不同的内存分配器。
- 线程安全策略: 一个类可以根据模板参数选择不同的同步机制。
- 错误处理策略: 定义不同的错误处理行为。
(3)接口强制执行:确保派生类实现特定的成员。
虚函数是实现接口的一种方式,但有运行时开销。CRTP 在编译时强制派生类实现特定接口的方法,不用虚函数。
实现方式: CRTP 基类模板在自己的方法中调用派生类预期存在的成员函数。如果派生类没有实现这些成员函数,编译器会在编译时报错,因为 static_cast<Derived*>(this) 后的调用失败。
典型应用:
- 迭代器适配器 : Boost 库的
boost::iterator_facade用 CRTP 提供大部分迭代器操作(++、--、*、==等),但要求派生类实现一些核心原语。 - 自定义容器。
(4)链式调用:返回正确的派生类型。
链式调用(也称为流式接口)是一种编程风格,在同一行代码连续调用多个方法,每个方法都返回对象自身(*this),形成一个“链”。继承时,如果基类方法返回 *this,会返回基类类型,会中断链式调用,丢失派生类的特定方法。CRTP 可以解决这个问题。
实现方式: CRTP 基类的方法返回 static_cast<Derived&>(*this),确保返回的是派生类的引用,链式调用继续在派生类的方法上进行。
典型应用:
- 构建器模式: 创建复杂对象用链式调用设置各种属性。
- 配置器: 连续设置对象的多个配置选项。
示例(链式调用):
template <typename Derived>
class ChainedBase {
public:
Derived& setCommonProperty(int value) {
// ... 设置通用属性 ...
std::cout << "Setting common property to " << value << std::endl;
return static_cast<Derived&>(*this);
}
};
class MyBuilder : public ChainedBase<MyBuilder> {
public:
MyBuilder& setSpecificProperty(const std::string& name) {
// ... 设置特定属性 ...
std::cout << "Setting specific property to " << name << std::endl;
return *this; // 返回自身,保持链式调用
}
void build() {
std::cout << "Building object..." << std::endl;
}
};
五、结语
CRTP 是 C++ 的高级编程。编译时利用类型信息,实现静态多态、代码复用和严格的类型检查。合理运用 CRTP,编写性能卓越的 C++ 代码。

873

被折叠的 条评论
为什么被折叠?



