当前位置:首页 » 《随便一记》 » 正文

C++惯用法之CRTP(奇异递归模板模式)

4 人参与  2024年02月28日 08:01  分类 : 《随便一记》  评论

点击全文阅读


相关系列文章

如何写出高质量的函数?快来学习这些coding技巧

C++惯用法之Pimpl

C++之数据转换(全)

目录

1.介绍

2.CRTP的使用场景

2.1.实现静态多态

2.2.代码复用和扩展性

3.总结


1.介绍

        CRTP的全称是Curiously Recurring Template Pattern,即奇异递归模板模式,简称CRTP。CRTP是一种特殊的模板技术和使用方式,是C++模板编程中的一种惯用法。基本特征表现为:基类是一个模板类;派生类在继承该基类时,将派生类自身作为模板参数传递给基类。下面用网上的实例来说明为什么要用CRTP? 比如我要实现一个数学库,如果使用运行时多态来实现向量类Vector,那么代码结构大致如下:

template<typename Type, unsigned Len>struct VectorBase{    virtual void someFunction(){...}    ...};struct Vector3: VectorBase<float, 3>{    virtual void someFunction() override {...}};

         需要注意的是,运行时多态是有开销的,熟悉c++虚函数的人应该就能明白,如果我调用一个虚函数,需要查询对象头部的虚函数表来得到实际函数地址,这个寻址的开销对于一个数学库而言是非常巨大的。而如果使用静态多态,则可以使用如下的代码来实现:

template <typename ChildType> struct VectorBase {  ChildType &underlying() { return static_cast<ChildType &>(*this); }  inline ChildType &operator+=(const ChildType &rhs) {    this->underlying() = this->underlying() + rhs;    return this->underlying();  }};struct Vec3f : public VectorBase<Vec3f> {  float x{}, y{}, z{};  Vec3f() = default;  Vec3f(float x, float y, float z) : x(x), y(y), z(z) {}};inline Vec3f operator+(const Vec3f &lhs, const Vec3f &rhs) {  Vec3f result;  result.x = lhs.x + rhs.x;  result.y = lhs.y + rhs.y;  result.z = lhs.z + rhs.z;  return result;}

        可以看到,静态多态虽然导致代码复用度相较于运行时多态降低了很多,但是相较于完全手写,我们可以利用子类实现的operator+来通过CRTP自动添加operator+=,相当于是做到了运行效率与开发效率的相对平衡。 

        VectorBase是模版基类,派生类Vec3f 继承自VectorBase,并以自身作为模板参数传递给基类, 在基类内部,通过使用static_cast,将this指针转换为模板参数类型T的指针,然后调用类型T的方法imp。static_cast的用法可参考一下博客。

C++之数据转换(全)_c++数据类型转换-CSDN博客

2.CRTP的使用场景

2.1.实现静态多态

C ++支持动态和静态多态。

动态多态性:在这种类型的多态性中,在编译时不知道对象的类型(可能基于用户输入等),因此编译器添加了额外的数据结构来支持这一点。 该标准并未规定应如何实施。C++通过虚函数表实现多态,但是虚函数会影响类的内存布局,并且虚函数的调用会增加运行时的开销。静态多态性:在这种类型中,对象的类型在编译时本身是已知的,因此实际上无需在数据结构中保存额外的信息。 但是如前所述,我们需要在编译时知道对象的类型。

CRTP 可以实现静态多态,但本质上 CRTP 中的多个派生类并不是同一个基类,因此严格意义上不能叫多态。示例如下:

template<typename T>class base{public:    virtual ~base(){}    void interface() { static_cast<T*>(this)->impl(); }    void impl() { cout << "base impl... " << endl; }};class derived1 : public base<derived1>{public:    void impl(){ cout << "derived1 impl... " << endl; }};class derived2 : public base<derived2>{public:    void impl() { cout << "derived2 impl..." << endl; }};template<typename T>void testDemo(T & base){    base.interface();}int main(){    derived1 a1;    derived2 a2;    testDemo(a1);  //输出:derived1 impl...     testDemo(a2);  //输出:derived2 impl...        return 0;}

2.2.代码复用和扩展性

使用 CRTP 可以把重复性的代码抽象到基类中,减少代码冗余。示例代码如下:

template<typename T>class base{public:    virtual ~baseDemo(){}    void getType()     {         T& t = static_cast<T&>(*this);        cout << typeid(t).name() << endl;    } };class derived1 : public base<derived1>{};class derived2 : public base<derived2>{};int main(){    derived1  a1;    derived2  a2;    a1.getType(); //输出:class derived1    a2.getType(); //输出:class derived2        return 0;}

        可以看到,在基类中getType函数中能够获取到派生于base所有类的类型信息,相比于虚函数的方式减少了代码。上面的代码在getType函数不变的情况下,可以任意编写base的扩展类,提高了代码的可重用性和灵活性。

        我们再看一个示例:多态拷贝构造(Polymorphic copy construction)

// Base class has a pure virtual function for cloningclass Shape {public:    virtual ~Shape() {};    virtual Shape *clone() const = 0;};// This CRTP class implements clone() for Derivedtemplate <typename Derived>class Shape_CRTP : public Shape {public:    virtual Shape *clone() const {        return new Derived(static_cast<Derived const&>(*this));    }}; // Nice macro which ensures correct CRTP usage#define Derive_Shape_CRTP(Type) class Type: public Shape_CRTP<Type> // Every derived class inherits from Shape_CRTP instead of ShapeDerive_Shape_CRTP(Square) {};Derive_Shape_CRTP(Circle) {};

         传统的实现方式是,基类有个虚拟clone函数,每个继承类实现自己的clone函数功能。依靠CRTP技术,只定义一个就够了,大家通用,一样减少了冗余代码,提高了代码的复用性。

        其实,CRTP使用到的地方很多,比如还可以实现一种多态的链式式编程。示例代码如下:

template <typename ConcretePrinter>class Printer{public:    Printer(std::ostream& pstream) : stream_(pstream) {}    template <typename T>    ConcretePrinter& print(T&& t)    {        stream_ << t;        return static_cast<ConcretePrinter&>(*this);    }    template <typename T>    ConcretePrinter& println(T&& t)    {        stream_ << t << std::endl;        return static_cast<ConcretePrinter&>(*this);    }private:    std::ostream& stream_;};enum  Color{    red,blue,green};class CoutPrinter : public Printer<CoutPrinter>{public:    CoutPrinter() : Printer(std::cout) {}    CoutPrinter& SetConsoleColor(Color c)    {        return *this;    }};void TestChain(){    CoutPrinter().print("Hello ").SetConsoleColor(Color::red).println("Printer!");}int main(){    TestChain();    return 0;}

从上面代码可以看出,都是从类成员函数中返回自身的引用,基类通过模版返回派生类的引用和派生类返回自己达到一样的目的。一般来说在c++里很少出现这种链式的代码,只有通过模版才能实现这种效果。这个跟函数实现链式表达式的原理差不多。

 如何写出高质量的函数?快来学习这些coding技巧-CSDN博客

3.总结

优点:省去动态绑定、查询虚函数表带来的开销。通过CRTP,基类可以获得到派生类的类型,提供各种操作,比普通的继承更加灵活。但CRTP基类并不会单独使用,只是作为一个模板的功能。缺点:使用CRTP需要编写更多的模板代码,增加了代码的复杂度,对于不熟悉模板编程的开发者来说可能会带来一定的学习成本。

点击全文阅读


本文链接:http://m.zhangshiyu.com/post/70733.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1