• C++ 模板 - CRTP 技法


    在C++中,前人总结了模板编程中非常多的技巧来用。在此其中,CRTP可以说是一种利用模板来达到编译期多态的用法。CRTP(Curiously Recurring Template Pattern),是一种利用模板和继承来达到提供class的额外内容、静态多态的手法。

    什么是多态?

    编程语言类型论中,多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口

    – 来自百度

    多态其实可以简单看成就是用户执行同样的行为,但是因为执行者的差异,展现出不同的效果

    通常语言中会使用继承和动态绑定来实习这一目的(当然,你个年轻人不讲武德,用函数绑定来干这一件事情另谈)

    常规多态

    class A {
    public:
      virtual void f() {std::cout << "A\n";}
    };
    
    class B: public A {
    public:
      void f() {std::cout << "B\n";}
    };
    
    class C: public A {
    public:
      void f() {std::cout << "C\n";}
    };
    
    void demo(A* a) {
      a->f();
    }
    
    int main() {
      demo(new A);
      demo(new B);
      demo(new C);
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    直接使用std::function

    第二种并算不上一种方法,但是在只有一个两个虚函数时还是有用途的

    class A {
    public:
      std::function<void()> f;
      explicit A(std::function<void()>&&fun): f(std::move(fun)){}
    };
    
    void demo(A* a) {
      a->f();
    }
    
    int main() {
      demo(new A([]() {std::cout << "A\n";}));
      demo(new A([]() {std::cout << "B\n";}));
      demo(new A([]() {std::cout << "C\n";}));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    上述都是迟绑定

    在c++里面,若是要实现多态的话,实现多态利用的虚表来实现的迟绑定(类似于std::function),虚表中存放好对应函数的调用地址,以此来实现运行时选择

    但是,运行时绑定也不是没有负担

    • C++中内存布局
    • 函数不能内联和优化

    虚表再怎么都需要占用一个指针的空间,不然,如何存放虚表指针

    由于是运行时决定,而不是早绑定,需要通过虚表去寻找调用地址,中间存在一个过程(虽然可以忽略不计,但是不计较今天怎么说CRTP呢?)。

    c++的虚表可以满足需求了

    c++是一门多样化的语言

    多样化让c++变得可以选择的方式变多了(比如上述的直接利用std::function来搞,你只管实现,剩下的交给编译器)

    CRTP,一般类似于这种形式

    template<typename T>
    class A {
    public:
      T* self() {return static_cast<T*>(this);}  
    };
    
    class B: public A<B>{
      
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    核心在于,父类是个模板,模板有个参数是子类

    对于父类来说,只需要知道子类的名称可以生成一个指针就可以了

    父类在实例化时只需要有子类的声明的就ok了,由于传入的模板类会继承自己,所以自己可以向下转这个指针。

    于是,我们就可以这样玩:

    template <typename D> class Base {
      void f_impl();
      int add_impl(int a, int b);
      D *self() { return static_cast<D *>(this); }
    
    protected:
      Base() {}
    
    public:
      void f() { self()->f_impl(); }
      int add(int a, int b) { return self()->add_impl(a, b); }
    };
    class A : public Base<A> {
    public:
      int x;
      A(int v): x(v) {}
      int add_impl(int a, int b) {return x + a + b;}
      void f_impl() { std::cout << "A\n"; }
    };
    
    class B : public Base<B> {
    public:
      int x;
      B(int v): x(v) {}
      int add_impl(int a, int b) {return x + a + b;}
      void f_impl() { std::cout << "B\n"; }
    };
    
    int main() {
      Base<A> *t = new A(1);
      t->f();
      std::cout << t->add(1, 2) << "\n";
      Base<B> *p = new B(1);
      p->f();
      std::cout << p->add(1, 2) << "\n";
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 不让父类生成对象
    • 父类只声明具体函数实现

    其实只是交给编译器,去codegen 生成了 几分不同的 base 的类

    所有行为发生在编译期

    上述main代码里面还是重复代码了,我们是“多态”,当然要做一个统一的动作呢

    统一动作呢,当然也要统一生成

    template<typename T>
    void f(Base<T>* base) {
      base->f();
      std::cout << base->add(1, 2) << "\n";
    }
    
    int main() {
      f(new A(1));
      f(new B(1));
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    其实核心观点在于,由于是静态绑定到函数上面的,不能够找到子类的类型是一定不行的。

    所以,父类保存一个转换子类指针的方法就可以解决这一件事情。

    这就是CRTP技法

    可以用到需要保存子类信息的地方

    更多的用法:enable_shared_from_this

    {amjieker}

  • 相关阅读:
    Ansible 常用命令50条
    【vue设计与实现】异步组件与函数式组件 1 - 异步组件要解决的问题和实现
    jenkins部署vue项目
    2023数学建模国赛C题赛后总结
    模型压缩常用方法简介
    Excel最基本的常用函数
    编译安装Erlang+RabbitMQ
    【网络层】子网划分、无分类编址CIDR、构成超网、ARP协议
    CSS设置字体样式
    SpringEvent 事件发布/监听机制相关源码解析
  • 原文地址:https://blog.csdn.net/qq_51986723/article/details/127723111