CRTP 奇特重现模板模式
它的范式基本上都是:父类是模板,再定义一个类类型继承它。因为类模板不是类,只有实例化后的类模板才是实际的类类型,所以我们需要实例化它,显式指明模板类型参数,而这个类型参数就是我们定义的类型,也就是子类了。
template <class Dervied>
class Base {};
class X : public Base<X> {};
这种范式是完全合法合理的,并无问题,首先不要对这种东西感到害怕或觉得非常神奇,也只不过是基本的语法规则所允许的。即使不使用 CRTP
,这些写法也是完全合理的,并无问题。
CRTP 可用于在父类暴露接口,而子类实现该接口,以此实现“编译期多态”,或称“静态多态”。 示例如下:
template <class Dervied>
class Base {
public:
// 公开的接口函数 供外部调用
void addWater(){
// 调用子类的实现函数。要求子类必须实现名为 impl() 的函数
// 这个函数会被调用来执行具体的操作
static_cast<Dervied*>(this)->impl();
}
};
class X : public Base<X> {
public:
// 子类实现了父类接口
void impl() const{
std::cout<< "X 设备加了 50 毫升水\n";
}
};
使用方式也很简单,我们直接创建子类对象,调用 addWater
函数即可:
X x;
x.addWater();
运行测试。
那么好,问题来了,为什么呢? static_cast<Dervied*>(this)
是做了什么,它为什么可以这样?
- 很显然
static_cast<Dervied*>(this)
是进行了一个类型转换,将this
指针(也就是父类的指针),转换为通过模板参数传递的类型,也就是子类的指针。 - 这个转换是安全合法的。因为 this 指针实际上指向一个 X 类型的对象,X 类型对象继承了
Base<X>
的部分,X 对象也就包含了Base<X>
的部分,所以这个转换在编译期是有效的,并且是合法的。 - 当你调用
x.addWater()
时,实际上是 X 对象调用了父类Base<X>
的成员函数。这个成员函数内部使用static_cast<X*>(this)
,将 this 从Base<X>*
转换为X*
,然后调用 X 中的 impl() 函数。这种转换是合法且安全的,且 X 确实实现了 impl() 函数。
当然了,我们给出的示例是十分简单的,不过大多的使用的确也就是如此了,我们可以再优化一点,比如不让子类的接口暴露出来:
template <class Dervied>
class Base {
public:
void addWater(){
static_cast<Dervied*>(this)->impl();
}
};
class X : public Base<X> {
// 设置友元,让父类得以访问
friend Base<X>;
// 私有接口,禁止外部访问
void impl() const{
std::cout<< "X 设备加了 50 毫升水\n";
}
};
使用 CRTP 模式实现静态多态性并复用代码
虚函数的价值在于,作为一个参数传入其他函数时 可以复用那个函数里的代码,而不需要在需求频繁变动与增加的时候一直修改。
class BaseVirtual {
public:
virtual void addWater(int amount) = 0; // 纯虚函数声明
};
class XVirtual : public BaseVirtual {
public:
void addWater(int amount) override {
std::cout << "XVirtual 设备加了 " << amount << " 毫升水\n";
}
};
class YVirtual : public BaseVirtual {
public:
void addWater(int amount) override {
std::cout << "YVirtual 设备加了 " << amount << " 毫升水\n";
}
};
// 接口,父类的引用
void processWaterAdditionVirtual(BaseVirtual& r, int amount) {
if (amount > 0) {
r.addWater(amount);
} else {
std::cerr << "无效数量: " << amount << '\n'; // 错误处理
}
}
int main(){
XVirtual xVirtual;
YVirtual yVirtual;
processWaterAdditionVirtual(xVirtual, 50);
processWaterAdditionVirtual(yVirtual, 100);
processWaterAdditionVirtual(xVirtual, -10);
}
运行测试。
CRTP 同样可以,并且还是静态类型安全,这不成问题:
template <typename Derived>
class Base {
public:
void addWater(int amount) {
static_cast<Derived*>(this)->impl_addWater(amount);
}
};
class X : public Base<X> {
friend Base<X>;
void impl_addWater(int amount) {
std::cout << "X 设备加了 " << amount << " 毫升水\n";
}
};
class Y : public Base<Y> {
friend Base<Y>;
void impl_addWater(int amount) {
std::cout << "Y 设备加了 " << amount << " 毫升水\n";
}
};
template <typename T>
void processWaterAddition(Base<T>& r, int amount) {
if (amount > 0) {
r.addWater(amount);
} else {
std::cerr << "无效数量: " << amount << '\n';
}
}
int main() {
X x;
Y y;
processWaterAddition(x, 50);
processWaterAddition(y, 100);
processWaterAddition(x, -10);
}
运行测试。
C++23 的改动-显式对象形参
C++23 引入了显式对象形参,让我们的 CRTP
的形式也出现了变化:
显式对象形参,顾名思义,就是将 C++23 之前,隐式的,由编译器自动将
this
指针传递给成员函数使用的,改成允许用户显式写明了,也就是:cppstruct X{ void f(this const X& self){} };
它也支持模板(可以直接
auto
而无需再template<typename>
),也支持各种修饰,如:this X self
、this X& self
、this const X& self
、this X&& self
、this auto&& self
、const auto& self
... 等等。
struct Base { void name(this auto&& self) { self.impl(); } };
struct D1 : Base { void impl() { std::puts("D1::impl()"); } };
struct D2 : Base { void impl() { std::puts("D2::impl()"); } };
不再需要使用 static_cast
进行转换,直接调用即可。且如你所见,我们的显式对象形参也可以写成模板的形式:this auto&& self
。
使用上也与之前并无区别,创建子类对象,调用接口即可。
D1 d;
d.name();
D2 d2;
d2.name();
d.name
也就是把 d
传入给父类模板成员函数 name
,auto&&
被推导为 D1&
,顾名思义”显式“对象形参,非常的简单直观。
运行测试。
CRTP 的好处
上一节我们详细的介绍和解释了 CRTP 的编写范式和原理。现在我们来稍微介绍一下 CRTP 的众多好处。
静态多态
CRTP 实现静态多态,无需使用虚函数,静态绑定,无运行时开销。
类型安全
CRTP 提供了类型安全的多态性。通过模板参数传递具体的子类类型,编译器能够确保类型匹配,避免了传统向下转换可能引发的类型错误。
灵活的接口设计
CRTP 允许父类定义公共接口,并要求子类实现具体的操作。这使得基类能够提供通用的接口,而具体的实现细节留给派生类。其实也就是说多态了。
总结
事实上笔者所见过的 CRTP
的用法也还不止如此,还有许多更加复杂,有趣的做法,不过就不想再探讨了,以上的内容已然足够。其它的做法也无非是基于以上了。
各位可以尝试尽可能的将使用虚函数的代码改成 CRTP
,这其实在大多数时候并不构成难度,绝大多数的多态类型都能被很轻松的改成 CRTP
的形式。