cxx CRTP
六月 13, 2022 [c++] #c++cxx CRTP
Curiously Recurring Template Pattern 的概念
C++ 通过类的继承与虚函数的动态绑定,实现了多态。这种特性,使得我们能够用基类的指针,访问子类的实例。例如我们可以实现一个名为 Animal 的基类,以及 Cat, Dog 等子类,并通过在子类中重载虚函数 jump,实现不同动物的跳跃动作。而后我们可以通过访问 Zoo 类的实例中存有 Animal 指针的数组,让动物园中所有的动物都跳一遍。
class Zoo {
...
private:
std::vector<shared_ptr<Animal>> animals;
public:
void () {
for (auto animal : animals) {
animal->jump();
}
}
...
}
在每次执行 animal->jump() 的时候,系统会检查 animal 指向的实例实际的类型,然后调用对应类型的 jump 函数。这一步骤需要通过查询虚函数表(vtable)来实现;由于实际 animal 指向对象的类型在运行时才确定(而不是在编译时就确定),所以这种方式称为动态绑定(或者运行时绑定)。
因为每次都需要查询虚函数表,所以动态绑定会降低程序的执行效率。为了兼顾多态与效率,有人提出了 Curiously Recurring Template Pattern 的概念。
通过模板实现静态绑定
把派生类作为基类的模板参数:父类是一个模板类,当子类继承父类时,传入的模板类型为子类本身(这样父类就可以调用子类的相关方法了(通过this))
为了在编译时绑定,我们就需要放弃 C++ 的虚函数机制,而只是在基类和子类中实现同名的函数;同时,为了在编译时确定类型,我们就需要将子类的名字在编译时提前传给基类。因此,我们需要用到 C++ 的模板。
#include <iostream>
using namespace std;
template<typename T>
class Base {
public:
void show () const {
static_cast<const T*>(this)->show();
}
};
class Derived: public Base<Derived> {
public:
// 这里子类中如果不定义 show; 则调用show时会递归的调用Base中的show,最后爆栈!!!!
void show () const {
cout << "Shown in Derived class." << endl;
}
};
int main () {
Derived d;
Base<Derived> *b = &d;
b->show();
return 0;
}
用在哪里?
现在我们考虑这样一个问题。
在使用虚函数的风格中,我们可以把 Cat , Dog... 等不同子类的指针,复制给基类的指针 Animal*,然后把基类的指针存入容器中(比如 vector<Animal*>)。但是,在 CRTP 中,我们就做不到这样了。这是因为同样是基类的指针 Animal<Cat>* 和 Animal<Dog>* 是两种完全不同的类型的指针。这样一来,我们就没法构造一个动物园了。
那么,CRTP 到底应该怎么用呢?我们不妨回过头来想一想,最初我们引入 CRTP 是为了什么。文章开头的第一段,我们提到多态是个很好的特性,但是动态绑定比较慢,因为要查虚函数表。而事实上,动态绑定慢,通常是因为多级继承;如果继承很短,那么查虚函数表的开销实际上也没多大。
在之前举出的例子里,我们运用 CRTP,完全消除了动态绑定;但与此同时,我们也在某种意义上损失了多态性。现在我们希望二者兼顾:保留多态性,同时降低多级继承带来的虚函数表查询开销。答案也很简单:让 CRTP 的模板类继承一个非模板的基类——这相当于这个非模板的基类会有多个平级的不同的子类。一个示例如下。
#include <iostream>
#include <vector>
using std::cout; using std::endl;
using std::vector;
class Animal {
public:
virtual void say () const = 0;
virtual ~Animal() {}
};
template <typename T>
class Animal_CRTP: public Animal {
public:
void say() const override {
static_cast<const T*>(this)->say();
}
};
class Cat: public Animal_CRTP<Cat> {
public:
void say() const {
cout << "Amimal_CRTP:\n";
cout << "Meow~ I'm a cat." << endl;
}
};
class Dog: public Animal_CRTP<Dog> {
public:
void say() const {
cout << "Wang~ I'm a dog." << endl;
}
};
int main () {
vector<Animal*> zoo;
zoo.push_back(new Cat());
zoo.push_back(new Dog());
// 这里并不会调用Animal_CRTP 的say函数
for (vector<Animal*>::const_iterator iter{zoo.begin()}; iter != zoo.end(); ++iter) {
(*iter)->say();
}
for (vector<Animal*>::iterator iter{zoo.begin()}; iter != zoo.end(); ++iter) {
delete (*iter);
}
return 0;
}
应用
IComparable
我们常常在Java和C的更加强大。
#include<iostream>
using namespace std;
template<typename T>
class IComparable
{
public:
bool less(const IComparable<T>& b) const {
return this->lessImpl(b);
}
bool greater(const IComparable<T>& b) const {
return b.less(*this);
}
bool notLess(const IComparable<T>& b) const {
return !this->less(b);
}
bool notGreater(const IComparable<T>& b) const {
return !this->greater(b);
}
bool equal(const IComparable<T>& b) const {
return !this->less(b) && !this->greater(b);
}
bool notEqual(const IComparable<T>& b) const {
return !this->equal(b);
}
private:
bool lessImpl(const IComparable<T>& b) const {
return true;
}
}; // Need Derived to override lessImpl().
class A : public IComparable<A>
{
public:
A(int num):N(num){}
bool lessImpl(const A& b){
return N < b.N;
}
private:
int N;
};
class B : public IComparable<B>
{
public:
B(int num1, int num2):N1(num1), N2(num2){}
bool lessImpl(const B& b){
return N1 < b.N1 || N2 < b.N2;
}
private:
int N1, N2;
};
int main(){
//cout << alphabeta;
A a(5), b(10);
cout << a.less(b) << endl; // OK
B c(5, 10), d(5, 0);
cout << c.greater(d) <<endl; // OK
}
这段代码里,我首先定义了一个接口IComparable
对于这个例子,可以发现:生成这些操作完全是在编译期完成的,在运行期只是普通的比较,且没有任何空间开销,虽然IComparable
基类能够在不直接知道派生类类型的情况下,为派生类提供功能,且无任何的运行时开销。
Counter
#include<iostream>
using namespace std;
template<typename T>
class Counter
{
public:
static size_t get(){
return Count;
}
Counter(){
add(1);
}
Counter(const Counter& other){
add(1);
}
~Counter(){
add(-1);
}
private:
static int Count;
static void add(int n){
Count += n;
}
};
template<typename T>
int Counter<T>::Count = 0;
class A : public Counter<A>
{};
class B : public Counter<B>
{};
int main(){
// Counter<A>::add(1); [Error] 'static void Counter<T>::add(int) [with T = A]' is private
A a1;
cout << "A : " << Counter<A>::get() << endl;
{
B b1;
cout << "B : " << Counter<B>::get() << endl;
{
A a2;
cout << "A : " << Counter<A>::get() << endl;
}
cout << "A : " << Counter<A>::get() << endl;
}
cout << "B : " << Counter<B>::get() << endl;
}
对象构造肯定会调用构造函数,析构时也会调用析构函数,我们只要在构造函数和析构函数中添加计数的代码,再让其他类继承。
在继承Counter
enable_shared_from_this
我们看C++标准库中,最为经典的就是enable_shared_from_this<T>。 为了实现从类中传出一个安全的shared_ptr<T>来包装this指针,并且只能对现有的类做极小的修改,对实际使用没有影响。 使用CRTP是最好的选择。通过继承enable_shared_from_this<T>,它可以自动生成派生类的传出this的方法。
#include <memory>
#include <iostream>
struct Good: std::enable_shared_from_this<Good> // 注意:继承
{
std::shared_ptr<Good> getptr() {
return shared_from_this();
}
};
struct Bad
{
// 错误写法:用不安全的表达式试图获得 this 的 shared_ptr 对象
std::shared_ptr<Bad> getptr() {
return std::shared_ptr<Bad>(this);
}
~Bad() { std::cout << "Bad::~Bad() called\n"; }
};
int main()
{
// 正确的示例:两个 shared_ptr 对象将会共享同一对象
std::shared_ptr<Good> gp1 = std::make_shared<Good>();
std::shared_ptr<Good> gp2 = gp1->getptr();
std::cout << "gp2.use_count() = " << gp2.use_count() << '\n';
// 错误的使用示例:调用 shared_from_this 但其没有被 std::shared_ptr 占有
try {
Good not_so_good;
std::shared_ptr<Good> gp1 = not_so_good.getptr();
} catch(std::bad_weak_ptr& e) {
// C++17 前为未定义行为; C++17 起抛出 std::bad_weak_ptr 异常
std::cout << e.what() << '\n';
}
// 错误的示例,每个 shared_ptr 都认为自己是对象仅有的所有者
std::shared_ptr<Bad> bp1 = std::make_shared<Bad>();
std::shared_ptr<Bad> bp2 = bp1->getptr();
std::cout << "bp2.use_count() = " << bp2.use_count() << '\n';
} // UB : Bad 对象将会被删除两次
还有就是C++20中的view_interface
由于ranges库还没有完全完成,暂无示例。
LLVM/MLIR
LLVM中大量使用了CRTP技术,如下随便截取一段代码:
amespace mlir {
class Operation final
: public llvm::ilist_node_with_parent<Operation, Block>,
private llvm::TrailingObjects<Operation, BlockOperand, Region,
detail::OperandStorage> {
public:
/// ...
MLIR中极其重要的数据结构之一mlir::Operation的声明处,可以看到它继承自一个模板基类。我们调到这个基类的地方:
template <typename NodeTy, typename ParentTy, class... Options>
class ilist_node_with_parent : public ilist_node<NodeTy, Options...> {
protected:
ilist_node_with_parent() = default;
private:
/// Forward to NodeTy::getParent().
///
/// Note: do not use the name "getParent()". We want a compile error
/// (instead of recursion) when the subclass fails to implement \a
/// getParent().
const ParentTy *getNodeParent() const {
return static_cast<const NodeTy *>(this)->getParent();
}
可以看到,在getNodeParent()接口中同样存在static_cast。整个开源项目中这样的用法数不胜数。
CRTP的意义
有人喜欢称CRTP为静态多态,不完全正确。我们把CRTP和Interface做个比较,CRTP是通过模板参数获得派生类的类型,并为每个派生类都生成了一个基类, 每个派生类继承CRTP类都要将自身的类型传给基类,就发生了模板实例化,本质上并没有一个基类(接口)对多个派生类(实现)。
而对于Interface,Interface本身不知道派生类的类型,但是它有固定的方法规定,参数个数,类型,返回值,都是确定的,并且每个方法都是通过按照vtable调用的,都是指针。 这样就能真正的做到一个基类(接口)对多个派生类(实现)。
将CRTP作为快速扩展类的手段,基类可以获得到派生类的类型,提供各种操作,比普通的继承更加灵活。但CRTP基类并不会单独使用,只是作为一个模板的功能。
链接
https://zhuanlan.zhihu.com/p/137879448 https://zhuanlan.zhihu.com/p/136258767