c++之CRTP

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++ 通过虚函数实现多态,但是虚函数会影响类的内存布局,并且虚函数的调用会增加运行时的开销。具体为:

  1. 每一个虚函数都需要额外得指针寻址
  2. 虚函数表现为多态时不能内敛,如果小函数多得话有比较大得性能损失
  3. 每个对象都需要额外得虚指针指向虚表

在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来达到多态目的。

参考

modern c++设计模式系列(一)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值