CRTP概述
CRTP,即奇异递归模板模式(Curiously Recurring Template Pattern),由James O. Coplien在
其1995年的论文中首次提出,是C++中一个独特而强大的设计模式。它利用模板和继承的特性,允许在编译时进行多态操作,从而提高代码的性能和灵活性。
代码示例
template<typename T>
class base {
public:virtual ~base(){}void interface(){static_cast<T*>(this)->imp();}
};class derived:public base<derived> {
public:void imp(){cout << " hello world " << endl;}
};
多态实现对比
C++ 通过虚函数实现多态,但是虚函数会影响类的内存布局,并且虚函数的调用会增加运行时的开销。具体为:
- 每一个虚函数都需要额外得指针寻址
- 虚函数表现为多态时不能内敛,如果小函数多得话有比较大得性能损失
- 每个对象都需要额外得虚指针指向虚表
在C++编程中,静态多态(Static Polymorphism)是一种使用模板实现的编译时多态。CRTP作为实现静态多态的有效方式,通过模板类和继承机制,使得子类可以在不增加运行时开销的情况下重用和扩展基类的功能,提高性能。
应用
1.将某个类变为单例
template<typename T>
class singlePatternTemplate
{
public:virtual ~singlePatternTemplate() {}singlePatternTemplate(const singlePatternTemplate&) = delete;singlePatternTemplate & operator=(const singlePatternTemplate&) = delete;static T& getSingleObj()
{static T obj;return obj;}
protected:singlePatternTemplate(){}
};class derivedDemo :public singlePatternTemplate<derivedDemo>
{friend singlePatternTemplate<derivedDemo>;
private:derivedDemo(){}
};
2.静态多态
#include <iostream>// CRTP基类模板template<typename Derived>class Shape {public:void draw() {static_cast<Derived*>(this)->doDraw(); // 静态转换为派生类并调用doDraw()}};// 派生类,继承自Shape模板,使用自身作为模板参数class Circle : public Shape<Circle> {
public:void doDraw() {std::cout << "Drawing a circle" << std::endl;}};class Square : public Shape<Square> {public:void doDraw() {std::cout << "Drawing a square" << std::endl;}};int main() {Circle circle;Square square;circle.draw(); // 输出 "Drawing a circle"square.draw(); // 输出 "Drawing a square"return 0;
}
CRTP与权限控制
当实现CRTP类时,我们不得不担心权限问题,任何你想要调用的方法都应该是可访问的。
对于CRTP方法必须是公共的或者调用方具体特殊的访问权限。这与虚函数调用权限有所不同:虚函数调用方必须有权限访问函数中需要的成员函数。
见如下代码:
template <typename D> class B {
public:void f(int i) { static_cast<D*>(this)->f_impl(i); }
private:void f_impl(int i) {}
protected:int i_;
};
class D : public B<D> {
private:void f_impl(int i) { i_ += i; }friend class B<D>; // 没有此句编译失败
};
f_impl为private,B必须有权限方法D中的成员函数,所以我们要通过友元的方式使调用方(这里是基类)有权限访问此派生类成员函数。
考虑如下D1派生类代码:
class D1 : public B<D> { // 注意这里继承public B<D>不是B<D1>
private:void f_impl(int i) { i_ -= i; }friend class B<D1>;
};
上述代码当对B<D1>进行调用时才会在编译期出现错误:
// 实际调用此句时编译才会失败,实际上不允许D1派生B<D>。
// 我们要考虑即使不调用此句,也需要编译器产生编译失败
// 提示开发者不能使用class D1 : public B<D>来编写D1
B<D1> *b = new D1;
b->f(1);
如果不调用B<D1>*b =new D1;是不会产生编译错误的,我们如果想在不调用B<D1>时就产生编译错误,怎么办呢?我们将属性变为私有,同时将模板类当作友元便可:
template<typename D>
class B {
private:int i_; // 注意i_原来是protected的,现在变为privatefriend D; // 模板作为友元public:void f(int i) { static_cast<D *>(this)->f_impl(i); }private:void f_impl(int i) {}private:int i_; // 由protected变为私有
};class D : public B<D> {...
}class D1 : public B<D> { // 注意这里继承public B<D>不是B<D1>
private:// 这时由于i_是私有属性,同时D1不是B的友元,// 所以这时候无论调用D1与否都会产生编译错误void f_impl(int i) { i_ -= i; }friend class B<D1>;
};
由于i_是私有属性,同时D1不是B的友元,所以这时候无论调用D1与否都会产生编译错误。
注意:
(1)这里不是为了D1能正常调用,B<D1>*b =new D1错误是因为B<D1>并不是D1的基类!
(2)如果不将i_声明为私有属性,那么只有明确写出B<D1>*b =new D1时才会出现编译错误
(3)现在的目标是即使不明确写出B<D1>*b =new D1也要编译器显式的编译错误提醒开发者不能实现class D1 : public B<D>。
标准库的使用
std::enable_shared_from_this部分源码实现
template<typename _Tp>class enable_shared_from_this{protected:enable_shared_from_this(const enable_shared_from_this&) noexcept { }~enable_shared_from_this() { }public:shared_ptr<_Tp>shared_from_this(){ return shared_ptr<_Tp>(this->_M_weak_this); }shared_ptr<const _Tp>shared_from_this() const{ return shared_ptr<const _Tp>(this->_M_weak_this); }private:mutable weak_ptr<_Tp> _M_weak_this;};
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);}
};
利用CRTP实现访问者模式
template<typename T>
struct visitor
{virtual void visit(T*) = 0;
};struct visitor_token{virtual ~visitor_token() = default;
};struct animal{virtual int move()=0;virtual void accept(visitor_token*) = 0;virtual ~animal() = default;
};//crtp
template<typename T>
struct visitable : public animal{void accept(visitor_token* v) override {dynamic_cast<visitor<T>*>(v)->visit(static_cast<T*>(this));}
};struct dog : public visitable<dog>{int move() override {return 4;}void swim(){std::cout<<"swim"<<std::endl;}
};struct bird : public visitable<bird>{int move() override {return 2;}void fly(){std::cout<<"fly"<<std::endl;}
};struct fish : public visitable<fish>{int move() override {return 1;}void dive(){std::cout<<"dive"<<std::endl;}
};template<class... T>
struct MultipleVisitor : public visitor_token, public visitor<T>...
{
};using MyVisitor = MultipleVisitor<dog,bird>;
using MyVisitor1 = MultipleVisitor<fish>;struct visitor_impl : public MyVisitor{void visit(dog* d) override{d->swim();}void visit(bird* b) override{b->fly();}
};struct visitor_impl1 : public MyVisitor1{void visit(fish* f) override{f->dive();}
};int main()
{animal* a = new dog;visitor_token* v = new visitor_impl;a->accept(v);animal* b = new bird;b->accept(v);visitor_token* v1 = new visitor_impl1;animal* c = new fish;c->accept(v1);
}
局限
既然CRTP能实现多态性,且其性能优于virtual,那么virtual还有没有存在的必要么?
虽然CRTP最终还是调用派生类中的成员函数。但是,问题在于Base类实际上是一个模板类,而不是一个实际的类。因此,如果存在名为Derived和Derived1的派生类,则基类模板初始化将具有不同的类型。这是因为,Base类将派生自不同的特化,即 Base,代码如下:
class Derived : public Base<Derived> {void imp(){std::cout << "in Derived::imp" << std::endl;}
};class Derived1 : public Base<Derived1> {void imp(){std::cout << "in Derived1::imp" << std::endl;}
};
如果创建Base类模板的指针,则意味着存在两种类型的Base指针,即:
// CRTP
Base<Derived> *b = new Derived;
Base<Derived> *b1 = new Derived1;
显然,这与我们虚函数的方式不同。因为,动态多态性只给出了一种Base指针。但是现在,每个派生类都可以使用不同的指针类型。
// virtual
Base *v1 = new Derived;
Base *v2 = new Derived1;
正是因为基于CRTP方式的指针具有不同的类型,所以不能将CRTP基类指针存储在容器中
,下面的代码将编译失败:
int main() {Base<Derived> *d = new Derived;Base<Derived> *d1 = new Derived1;auto vec = {d, d1};return 0;
}
编译器输出如下:
test.cc: In function ‘int main()’:
test.cc:33: error: cannot convert ‘Derived1*’ to ‘Base<Derived>*’ in initialization
test.cc:35: error: ISO C++ forbids declaration of ‘vec’ with no type
test.cc:35: error: scalar object ‘vec’ requires one element in initializer
正是因为其局限性,所以CRTP是一种特殊类型的多态性,在少数情况下可以替代动态多态性的需要。
输出结果完全符合预期,但是这样实现,可能存在以下两个问题:
• 性能损失:因为使用了virtual来实现此功能,而virtual函数会涉及到vtables等,所以如果频繁调用,性能会有影响
• 重复代码:为了实现这个功能,Derived和Derived1都需要在其函数体内实现PrintType()函数,如果派生类非常多的话,每个派生类都要实现该功能,冗余代码太多
总结
通过CRTP技术,在某种程度上也可以实现多态功能,但其也仅限定于使用场景,正如局限性一节中所提到的,CRTP是一种特殊类型的多态性,在少数情况下可以替代动态多态性的需要;另外,使用CRTP技术,代码可读性降低、模板实例化之后的代码膨胀以及无法动态绑定(在编译期决实例化),因此,我们可以根据使用场景,来灵活选择CRTP或者virtual来达到多态目的。
参考
modern c++设计模式系列(一)