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的,现在变为private
friend 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来达到多态目的。