19、奇异递归模板模式(CRTP):静态多态、代码复用、计数器模式
CRTP,全称是 Curiously Recurring Template Pattern。名字挺长,说白了就是:派生类把自己作为模板参数传给基类。
我第一次看到这种写法时,心里想的是:「这玩意儿不会死循环吗?」
后来用多了才发现——它不但不死循环,反而是 C++ 模板元编程里最实用的技巧之一。
今天我们就来聊聊 CRTP 的三个核心应用场景:静态多态、代码复用、计数器模式。
19.1 什么是 CRTP?
先看一个最简单的例子:
template <typename Derived>
class Base {
public:
void interface() {
static_cast<Derived*>(this)->implementation();
}
};
class Derived : public Base<Derived> {
public:
void implementation() {
std::cout << "Derived implementation\n";
}
};
注意看:Derived 继承自 Base<Derived>。这就是 CRTP 的核心——派生类把自己作为模板参数传给基类。
基类里通过 static_cast<Derived*>(this) 来调用派生类的成员函数。这个转换在编译期就完成了,没有虚函数开销。
19.2 静态多态:没有虚函数的多态
传统多态靠虚函数,运行时查虚表。CRTP 的多态在编译期就确定了。
我举个例子。假设我们要写一个「比较器」框架:
template <typename Derived>
class Comparator {
public:
bool operator!=(const Derived& other) const {
return !static_cast<const Derived*>(this)->operator==(other);
}
bool operator>(const Derived& other) const {
return other < *static_cast<const Derived*>(this);
}
// 其他比较运算符...
};
class IntWrapper : public Comparator<IntWrapper> {
int value_;
public:
explicit IntWrapper(int v) : value_(v) {}
bool operator==(const IntWrapper& other) const {
return value_ == other.value_;
}
bool operator<(const IntWrapper& other) const {
return value_ < other.value_;
}
};
你只需要实现 == 和 <,基类自动帮你生成 !=、>、>=、<=。
我在项目中遇到过类似的需求——需要为几十种数值类型提供完整的比较运算符。用 CRTP 写一次基类,所有派生类自动补齐,省了至少一半的代码量。
19.3 代码复用:把公共逻辑抽到基类
CRTP 的第二个大用处是代码复用。你想想看,很多类都有相同的「骨架」,只是「血肉」不同。CRTP 可以把骨架抽出来。
比如,我们要实现一个「单例模式」:
template <typename T>
class Singleton {
public:
static T& instance() {
static T obj;
return obj;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
protected:
Singleton() = default;
};
class Logger : public Singleton<Logger> {
friend class Singleton<Logger>;
// Logger 自己的实现...
public:
void log(const std::string& msg) { /* ... */ }
};
所有单例类只需要继承 Singleton<Derived>,自动获得 instance() 方法。注意 friend class Singleton<Logger> 这一行——因为基类的构造函数是 protected,派生类需要让基类能访问它。
再比如,对象池、工厂模式、访问者模式……很多设计模式都可以用 CRTP 来复用公共逻辑。
19.4 计数器模式:编译期自增 ID
这个用法比较冷门,但非常巧妙。CRTP 可以配合静态成员实现「编译期计数器」。
看代码:
template <typename T>
class Counter {
public:
static int count() { return counter_; }
protected:
Counter() { ++counter_; }
~Counter() { --counter_; }
private:
static inline int counter_ = 0;
};
class Widget : public Counter<Widget> { /* ... */ };
class Gadget : public Counter<Gadget> { /* ... */ };
每个派生类都有自己的 counter_ 静态变量。因为 Counter<Widget> 和 Counter<Gadget> 是不同的模板实例化,它们的静态成员互不干扰。
你可以用这个来:
- 统计某个类的当前存活对象数量
- 给每个对象分配一个自增 ID
- 实现对象池的引用计数
我曾经用这个模式写过一个调试工具——在析构函数里打印当前对象数量,排查内存泄漏。效果拔群。
__COUNTER__ 宏或者更复杂的模板技巧。
19.5 知识体系总览
下面这张图总结了 CRTP 的三大应用场景和它们之间的关系:
19.6 总结与避坑
CRTP 是 C++ 模板元编程里「性价比」最高的技巧之一。它不复杂,但能解决很多实际问题。
最后列几个我踩过的坑:
- 不要滥用 dynamic_cast:CRTP 基类里用
static_cast就够了,别用dynamic_cast,那会引入运行期开销。 - 注意友元声明:如果基类的构造函数是 protected,派生类需要
friend class Base<Derived>才能让基类访问。 - 模板代码膨胀:每个不同的模板参数都会生成一份独立的代码。如果派生类很多,注意二进制体积。
- 不要和虚函数混用:CRTP 和虚函数是两种不同的多态机制。混用会让代码难以理解,而且可能失去性能优势。
嗯,CRTP 就聊到这里。它不是什么高深莫测的技术,但用好了,能让你的代码更干净、更高效。
公众号:蓝海资料掘金营,微信deep3321