什么是CRTP
CRTP全称是curious recurring template pattern,即奇异递归模版模式,是一种c++的设计模式,精巧地结合了继承和模板编程的技术。可以用来给c++的class提供额外功能、实现静态多态等。
在CRTP之前只听说C++通过指针实现了动态多态,现在居然搞出了一个静态多态的东西出来?不得不感慨C++真是一门高深莫测的语言...
动态多态
在了解静态多态之前我们先来回顾一下动态多态,C++ 通过类的继承与虚函数的动态绑定,实现了多态。这种特性,使得我们能够用基类的指针,访问子类的实例。
c
#include <iostream>
class Animal{
public:
// 注意需要添加virtual关键字
virtual void run(){
std::cout << "Animal run" << std::endl;
}
};
class Cat:public Animal{
public:
void run() override{
std::cout << "Cat run" << std::endl;
}
};
int main() {
std::vector<Animal> animalVec;
animalVec.emplace_back(Animal());
// 非指针的形式,其实内部调用的还是Animal的run
animalVec.emplace_back(Cat());
for (auto animal:animalVec) {
animal.run();
}
// 动态多态需要通过指针的形式实现
std::vector<Animal*> animalVecPtr;
animalVecPtr.push_back(new Animal());
animalVecPtr.push_back(new Cat());
for (auto animal:animalVecPtr) {
animal->run();
}
return 0;
}
CRTP的一个重要功能就是用来实现静态多态,CRTP在编译阶段就将子类类型以模版的形式传递到父类,以便在编译阶段实现多态性,这就是静态多态。
既然有了动态多态,为什么还需要静态多态呢?答案是精益求精,为了效率而生...
我们知道动态多态是基于虚函数的形式在运行时进行动态绑定的,因此每次运行时都需要查询虚函数表,所以动态绑定会降低程序的执行效率。 为了兼顾多态与效率,就提出了CRTP。
CRTP的使用
我们先来看看在cppreference中是如何使用CRTP的
下面我们依然使用上面Animal的例子通过CRTP的方式实现静态多态。
首先我们按照官方的例子,依瓢画葫芦:
arduino
#include <iostream>
template < class T >
class Animal{
public:
virtual ~Animal(){
};
// CRTP这里已经不需要使用virtual关键字了
void run(){
(static_cast<T*>(this))->run();
}
};
class Cat:public Animal<Cat>{
public:
void run(){
std::cout << "Cat run" << std::endl;
}
};
class Dog:public Animal<Dog>{
public:
void run(){
std::cout << "Dog run" << std::endl;
}
};
int main() {
Animal<Cat>* cat = new Cat;
cat->run();
delete cat;
Animal<Dog>* dog = new Dog;
dog->run();
delete dog;
return 0;
}
程序运行起来后打印如下:
可以发现通过CRTP我们不使用关键字virtual
也能实现了通过父类指针调用子类方法效果,这就是静态多态的优点,它比动态多态更高效,更安全。
通过上面的例子我们总结一下使用CRTP的三个重要步骤:
-
继承自模版类,因为用到了继承,因此析构函数需要用virtual修饰,以避免内存泄露。
-
子类将自身通过模板参数传递给父类。
-
父类通过static_cast关键字将模板参数静态转化成子类,然后调用子类的鸭子模型方法。
一般来说将父类转换成子类一般使用的是dynamic_cast,而CRTP是在编译期间就已经明确知道了子类的具体类型,因此直接使用static_cast更为高效。 这也正是CRTP这种设计的一大精髓。
通过仔细对比我们动态多态和静态多态的两个例子我们发现还是有点不一样的,我们在动态多态中将Animal的指针添加到了std::vector中去,那么我们的CRTP能否也这样做呢? 我们来试一下:
arduino
#include <iostream>
template<class T>
class Animal {
public:
virtual ~Animal() {
};
// CRTP这里已经不需要使用virtual关键字了
void run() {
(static_cast<T *>(this))->run();
}
};
class Cat : public Animal<Cat> {
public:
void run() {
std::cout << "Cat run" << std::endl;
}
};
class Dog : public Animal<Dog> {
public:
void run() {
std::cout << "Dog run" << std::endl;
}
};
int main() {
std::vector<Animal<Cat>*> animalVec;
animalVec.emplace_back(new Cat());
// 报错了,因为vector存放的数据类型是Animal<Cat>
animalVec.emplace_back(new Dog());
for (auto animal: animalVec) {
animal->run();
}
return 0;
}
我们发现报错了,因为Animal和Animal不是同样的数据类型,不能同时放入同一个vector中去。 既然问题的根源是他们不是同样的数据类型,那么我们将它们变成同样的数据类型不就是行了吗?那么怎么把它们变成同样的数据类型呢?
让它们继承一个共同的基类即可。这样就是动态多态与静态多态结合使用的例子了。
实例代码如下:
arduino
#include <iostream>
class BaseAnimal {
public:
virtual ~BaseAnimal() {
};
virtual void run() = 0;
};
template<class T>
class Animal: public BaseAnimal{
public:
virtual ~Animal() {
};
// CRTP这里已经不需要使用virtual关键字了
void run() override{
(static_cast<T *>(this))->run();
}
};
class Cat : public Animal<Cat> {
public:
void run() override {
std::cout << "Cat run" << std::endl;
}
};
class Dog : public Animal<Dog> {
public:
void run() override {
std::cout << "Dog run" << std::endl;
}
};
int main() {
std::vector<BaseAnimal*> animalVec;
animalVec.emplace_back(new Cat());
// 报错了,因为vector存放的数据类型是Animal<Cat>
animalVec.emplace_back(new Dog());
for (auto animal: animalVec) {
animal->run();
}
return 0;
}
这样一来,我们通过CRTP与虚函数结合,即保留了动态多态的各种特性,也减少了部分虚函数的查找开销。
推荐阅读
关注我,一起进步。