CRTP (Curiously Recurring Template Pattern),奇异递归模板模式,是 C++ 中一种高级模板技术。
核心思想
CRTP 的核心思想是一个类 Derived
从一个以 Derived
自身作为模板参数的模板类 Base
派生。
以下是一个其写法:
1 2 3 4 5 6 7 8
| template <typename T> class Base { };
class Derived : public Base<Derived> { };
|
在这里,Derived
类继承自 Base<Derived>
,也就是 Base
模板类的一个特化版本,而这个特化版本恰好是以 Derived
自身作为模板参数的。这就是“奇异递归”的由来。
CRTP的作用与优势
静态多态(Compile-Time Polymorphism)
CRTP 允许在编译时实现多态行为,而不是运行时。这意味着可以避免虚函数(virtual
关键字)带来的运行时开销(如虚函数表查找)。基类 Base<T>
可以调用 T
类型(即派生类)的成员函数,而编译器在编译时就知道 T
是什么类型,从而直接生成对派生类函数的调用。
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
| #include <iostream>
template <typename T> class CounterBase { public: void increment() { static_cast<T*>(this)->doIncrement(); } void decrement() { static_cast<T*>(this)->doDecrement(); } protected: void doIncrement() { } void doDecrement() { } };
class MyCounter : public CounterBase<MyCounter> { public: MyCounter() : count(0) {} int getCount() const { return count; }
private: int count; friend class CounterBase<MyCounter>;
void doIncrement() { count++; std::cout << "MyCounter incremented to: " << count << std::endl; } void doDecrement() { count--; std::cout << "MyCounter decremented to: " << count << std::endl; } };
int main() { MyCounter mc; mc.increment(); mc.increment(); mc.decrement(); return 0; }
|
用于创建Mixin类
Mixin 是一种软件设计模式,它允许一个类“混合”或“注入”额外的功能到另一个类中,而无需使用多重继承(特别是在多重继承可能导致菱形继承问题或复杂性时)。通过 Mixin,可以将一组特定的行为或接口封装在一个独立的类中,然后让其他类继承这个 Mixin 类,从而获得这些行为。
CRTP 是实现 Mixin 的一种非常优雅和高效的方式。当一个类 Derived
继承自 Mixin<Derived>
时,Mixin
类可以利用 Derived
的类型信息,在编译时为 Derived
类提供通用的功能实现,同时要求 Derived
类实现一些特定的“钩子”方法。这样,Mixin
类提供了通用的骨架,而 Derived
类提供了具体的细节。
现在用这样的一个例子:实现一个Comparable
Mixin。这样如果一个子类需要实现比较操作时,只需要实现一个operator<
即可完成所有的5类比较运算符。
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 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| #include <iostream> #include <string>
template <typename T> class Comparable { public: bool operator>(const T& other) const { return other < static_cast<const T&>(*this); }
bool operator==(const T& other) const { return !(static_cast<const T&>(*this) < other) && !(other < static_cast<const T&>(*this)); }
bool operator!=(const T& other) const { return !(static_cast<const T&>(*this) == other); }
bool operator<=(const T& other) const { return !(static_cast<const T&>(*this) > other); }
bool operator>=(const T& other) const { return !(static_cast<const T&>(*this) < other); }
};
class MyInt : public Comparable<MyInt> { private: int value; public: MyInt(int v) : value(v) {}
bool operator<(const MyInt& other) const { return this->value < other.value; }
int getValue() const { return value; } };
class MyString : public Comparable<MyString> { private: std::string text; public: MyString(const std::string& s) : text(s) {}
bool operator<(const MyString& other) const { return this->text < other.text; }
const std::string& getText() const { return text; } };
int main() { MyInt i1(10), i2(20), i3(10); std::cout << "MyInt comparisons:" << std::boolalpha << std::endl; std::cout << "i1 < i2: " << (i1 < i2) << std::endl; std::cout << "i1 > i2: " << (i1 > i2) << std::endl; std::cout << "i1 == i3: " << (i1 == i3) << std::endl; std::cout << "i2 != i3: " << (i2 != i3) << std::endl; std::cout << "i1 <= i3: " << (i1 <= i3) << std::endl; std::cout << "i2 >= i1: " << (i2 >= i1) << std::endl;
std::cout << "\nMyString comparisons:" << std::boolalpha << std::endl; MyString s1("apple"), s2("banana"), s3("apple"); std::cout << "s1 < s2: " << (s1 < s2) << std::endl; std::cout << "s1 > s2: " << (s1 > s2) << std::endl; std::cout << "s1 == s3: " << (s1 == s3) << std::endl;
return 0; }
|
在这个例子中,Comparable
类是一个 Mixin。它通过 CRTP 接收派生类 T
作为模板参数,并利用 static_cast<const T&>(*this)
在编译时调用 T
中实现的 operator<
。这样,MyInt
和 MyString
只需实现一个 operator<
,就能自动获得所有其他比较操作符的功能,大大减少了代码重复。
策略模式
策略模式是一种行为设计模式,它允许在运行时选择算法的行为。然而,在 C++ 中,通过模板和 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 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
| #include <iostream> #include <string> #include <typeinfo>
template <typename T> class ConsoleLoggerPolicy { public: void log_creation() { std::cout << "[ConsoleLog] " << typeid(T).name() << " object created." << std::endl; } void log_destruction() { std::cout << "[ConsoleLog] " << typeid(T).name() << " object destroyed." << std::endl; } void log_action(const std::string& action) { std::cout << "[ConsoleLog] " << typeid(T).name() << ": " << action << std::endl; } };
template <typename T> class NoLoggerPolicy { public: void log_creation() {} void log_destruction() {} void log_action(const std::string& action) {} };
template <typename T, typename LoggingPolicy> class MyComponent : public LoggingPolicy { public: MyComponent() { LoggingPolicy::log_creation(); }
~MyComponent() { LoggingPolicy::log_destruction(); }
void performOperation() { LoggingPolicy::log_action("Performing a generic operation."); } };
class SpecificWidget : public MyComponent<SpecificWidget, ConsoleLoggerPolicy<SpecificWidget>> { public: SpecificWidget() { LoggingPolicy::log_action("SpecificWidget initialized."); }
void doSomethingSpecific() { LoggingPolicy::log_action("Doing something specific in SpecificWidget."); } };
class SilentGadget : public MyComponent<SilentGadget, NoLoggerPolicy<SilentGadget>> { public: SilentGadget() { LoggingPolicy::log_action("SilentGadget initialized."); } };
int main() { std::cout << "--- SpecificWidget (with ConsoleLoggerPolicy) ---" << std::endl; { SpecificWidget widget; widget.performOperation(); widget.doSomethingSpecific(); }
std::cout << "\n--- SilentGadget (with NoLoggerPolicy) ---" << std::endl; { SilentGadget gadget; gadget.performOperation(); }
return 0; }
|
在这个例子中:
ConsoleLoggerPolicy
和 NoLoggerPolicy
是两种不同的日志策略。它们都以 CRTP 方式接受它们所服务的具体类 T
作为模板参数,以便在日志消息中包含类型信息。
MyComponent
是一个通用组件,它通过模板参数 LoggingPolicy
注入日志功能。MyComponent
继承自 LoggingPolicy
,从而可以直接调用策略中定义的方法(如 log_creation()
)。
SpecificWidget
和 SilentGadget
是 MyComponent
的具体实例化,它们在编译时选择了不同的日志策略。
这种模式允许你在编译时灵活地切换组件的行为(这里是日志行为),而无需任何运行时开销(没有虚函数表查找)。当策略需要访问或了解其所注入的宿主类(例如 typeid(T).name()
)时,CRTP 的特性就显得尤为重要。
编译期接口检查
基类可以强制派生类实现某些方法。如果派生类没有实现基类期望的方法,编译就会失败,从而在编译期捕获错误。
避免代码重复
将通用逻辑放在基类中,派生类只需实现其特有的部分,减少代码重复。
缺点
不是运行时多态
CRTP 提供的多态是编译时多态。你不能像使用虚函数那样,通过一个 Base*
指针在运行时指向不同的派生类对象并调用其方法。例如,Base<SomeDerived>* ptr = new SomeDerived();
这样的用法是有效的,但 Base<AnotherDerived>* ptr2 = new AnotherDerived();
,这两个 Base
类型是完全不同的,它们没有共同的非模板基类。
其他
其他的还有比如理解复杂,模板编译时间会比较多,但是个人认为这个问题不是最主要的问题,相对于它带来的便捷性,是可以做一些牺牲的