浅谈 CRTP:奇异递归模板模式

Posted 鱼竿钓鱼干

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈 CRTP:奇异递归模板模式相关的知识,希望对你有一定的参考价值。

浅谈 CRTP:奇异递归模板模式

前言

建议先看一遍文末的参考资料!
建议先看一遍文末的参考资料!
建议先看一遍文末的参考资料!

思维导图

一、CRTP 是什么

CRTP 全称 : Curiously Recurring Template Pattern,也就是常说的奇异递归模板模式

下面先给出 CRTP 的一般形式

// The Curiously Recurring Template Pattern (CRTP)
template<class T>
class Base

    // methods within Base can use template to access members of Derived
;
class Derived : public Base<Derived>

    // ...
;

看了上面的代码是否觉得和有点熟悉又优点陌生

熟悉

  • 熟悉的模板
  • 熟悉的继承
  • 看起来和 std::enable_shared_from_this 差不多(实际上也是 CRTP 的一种应用,后面会具体讲解)

陌生
看起来好像自己继承自己好怪啊

class Derived : public Base<Derived>

下面谈谈为何要这么做

二、为什么要用 CRTP

2.1 CRTP 实现了静态多态

CRTP 通过将 派生类作为基类的模板参数实现了静态多态

2.1.1 什么是多态

面向对象 OOP 思想三大要点:封装、继承、多态

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

在 C++ 中有静态多态和动态多态两种实现方式,下面逐个来介绍

2.1.2 什么是动态多态

动态多态(动态绑定):即运行时的多态,在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。

C++ 通过虚函数实现动态多态,下面给出案例代码,如果你感觉代码理解有困难。你可以通过这篇文章简单复习一下
C++ 多态 - Arkin的文章 - 知乎

注意区分

  • 重写
  • 重载
  • 隐藏
#include<iostream>
using namespace std;

class Base

public:
	virtual void f(float x)
	
		cout<<"Base::f(float)"<< x <<endl;
	
	void g(float x)
	
		cout<<"Base::g(float)"<< x <<endl;
	
	void h(float x)
	
		cout<<"Base::h(float)"<< x <<endl;
	
;
class Derived : public Base

public:
    //子类与基类函数同名,有virtual关键字,运行时多态
	virtual void f(float x) override
	
		cout<<"Derived::f(float)"<< x <<endl;   //多态、覆盖
	
    //子类与基类函数同名,且无virtual关键字,隐藏
    //参数不同的隐藏
	void g(int x) 
	
		cout<<"Derived::g(int)"<< x <<endl;     //隐藏
	
    //参数相同的隐藏
	void h(float x)
	
		cout<<"Derived::h(float)"<< x <<endl;   //隐藏
	
;
int main(void)

	Derived d;        //子类对象
	Base *pb = &d;    //基类类型指针,指向子类对象
	Derived *pd = &d; //子类类型指针,指向子类对象
	// Good : behavior depends solely on type of the object
	pb->f(3.14f);   // Derived::f(float) 3.14  调用子类方法,多态
	pd->f(3.14f);   // Derived::f(float) 3.14  调用自己方法

	// Bad : behavior depends on type of the pointer
	pb->g(3.14f);   // Base::g(float)  3.14 
	pd->g(3.14f);   // Derived::g(int) 3 

	// Bad : behavior depends on type of the pointer
	pb->h(3.14f);   // Base::h(float) 3.14
	pd->h(3.14f);   // Derived::h(float) 3.14
	return 0;

2.1.3 如何实现动态多态

既然知道是通过虚函数来实现多态,那么具体的过程是怎么样的?为什么通过指针调用虚函数就能知道他到底是运行父类的虚函数还是子类的虚函数?
这和 C++ 的对象模型有关,具体是一个查找虚表的过程,如果您对相关概念还不了解可以去看看 侯捷先生面向对象相关的课程,下面我简单放几张图片做一个简短的介绍

个人相关笔记: ohmyfish C++ 侯捷 对象模型笔记

关于 vptr 和 vtbl

只要类里面有虚函数,类里就会有一个指针(无论有多少个虚函数),这个指针就是虚指针,虚指针指向虚函数表
父类有虚函数,子类也一定有。继承会把数据和函数的调用权都继承下来
当我们用指针调用的时候会发生动态绑定,首先通过指针找到vptr,然后找到vtbl,最后调用要求的函数
我们可以用C来模拟动态绑定的路线

//n是虚函数在虚函数表中的第几个,编译器按代码顺序放
(*(p->vptr)[n])(p);
(*p->vptr[n])(p);

以一个画板程序为例子,我们可以在容器里放指针。然后利用继承+虚函数实现一个多态,调用各自的draw

这比if-else更好一些,具体好在哪里可以学一下设计模式

关于 this

这个案例里:框架里把一些固定的、确定的步骤写好了,但是有一些操作还不确定要看应用具体怎么做(可以先去看一下设计模式的Template Method)

这时候我们就可以利用虚函数实现一个延后,把具体操作的实现延后到调用的时候,谁调用谁负责实现

然后再来看看this,我们可以认为this是调用者的地址,是一个指针

CMyDoc myDoc;
myDoc.OnFileOpen();//成员函数隐藏了一个this,注意啊这里还是对象调用而且OnFileOpen自己不是虚函数,所以这里是静态调用
myDoc.OnFileOpen(this);
myDoc.OnFileOpen(&myDoc);
myDoc.CDocument::OnFileOpen(&myDoc);//子类可以用父类的函数
//接下来就会对Serialize()进行动态绑定
this->Serialize();//this是子类对象
(*(this->vptr)[n])(this);//虚函数b

关于 Dynamic Binding

C++ 编译器看到一个函数调用有两种套路

  • 静态绑定:call xxx,一定调用到某个地址
  • 动态绑定:如果是通过指针调用虚函数并且该指针向上转型(upcast,比如指针是动物,然后new一只猪),那么编译器就会把调用动作编译成类似C语言版本来模拟调用路线。调用哪个地址要看指针指向什么

来看看汇编视角下的静态绑定:call xxx

汇编视角下的动态绑定

2.1.4 什么是静态多态

静态多态:也称为编译期间的多态,编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。

静态多态有两种实现方式:

  • 函数重载:包括普通函数的重载和成员函数的重载
  • 函数模板:包括普通的模板和本次要重点介绍的 CRTP 奇异递归模板模式

对于函数重载与普通模板实现静态多态这里不做详细介绍,只给出几个代码示例

函数重载:普通函数

#include <iostream>

int Volume(int s)   // 立方体的体积。
  return s * s * s;


double Volume(double r, int h)   // 圆柱体的体积。
  return 3.1415926 * r * r * static_cast<double>(h);


long Volume(long l, int b, int h)   // 长方体的体积。
  return l * b * h;


int main() 
  std::cout << Volume(10);
  std::cout << Volume(2.5, 8);
  std::cout << Volume(100l, 75, 15);

函数重载:成员函数

函数的参数类型和数目不同,与函数返回值类型没有关系。重载和成员函数是否是虚函数无关。

特征:

  • 相同的范围(在同一个类中)
  • 相同的函数名字
  • 不同的参数列表
  • virtual关键字可有可无
class A 
// 下面四个都是函数重载
	virtual int fun();
	void fun(int);
	void fun(double,double);
	static int fun(char);
;

普通模板

template <typename T>
void Swap(T &a,T &b)
	T temp;
	temp=a;
	a=b;
	b=temp;

下面来详细介绍如何通过 CRTP 来实现静态多态

2.1.5 如何通过 CRTP 实现静态多态(CRTP 原理介绍)

template <class T> 
struct Base

    void interface()
    
        // 不用 dynamic_cast 因为主要用在运行时,模板实在编译时就转换的
        static_cast<T*>(this)->implementation();
        // ...
    

    static void static_func()
    
        // ...
        T::static_sub_func();
        // ...
    
;

struct Derived : Base<Derived>

    void implementation();
    static void static_sub_func();
;

维基百科
基类模板利用了其成员函数体(即成员函数的实现)在声明之后很久都不会被实例化(实际上只有被调用的模板类的成员函数才会被实例化),并利用了派生类的成员函数(通过类型转化)。

在上例中,Base::interface(),虽然是在struct Derived之前就被声明了,但未被编译器实例化直至它被实际调用,这发生于Derived声明之后,此时Derived::implementation()的声明是已知的。

这种技术获得了类似于虚函数的效果,并避免了动态多态的代价。也有人把CRTP称为“模拟的动态绑定”。

下面利用 C++ Insights 针对具体例子分析一下

调用模板类成员函数前

#include<iostream>
using namespace std;

template<typename T>
struct Base 
    void interface() 
        static_cast<T*>(this)->implementation();	
    
    
    int get() const 
        return m_count;
    

    int m_count = 0;
;

struct Derived : Base<Derived> 
    void implementation() 
        m_count = 1;
    
;

int main() 
    Base<Derived>* b = new Derived;
//    b->interface();
//    cout << b->get() << endl;

    return 0;

insights.cpp

#include<iostream>
using namespace std;

template<typename T>
struct Base

  inline void interface()
  
    static_cast<T *>(this)->implementation();
  
  
  inline int get() const
  
    return this->m_count;
  
  
  int m_count = 0;
;

/* First instantiated from: insights.cpp:17 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct Base<Derived>

  inline void interface();
  
  inline int get() const;
  
  int m_count = 0;
  // inline constexpr Base() noexcept = default;
;

#endif


struct Derived : public Base<Derived>

  inline void implementation()
  
    /* static_cast<Base<Derived> *>(this)-> */ m_count = 1;
  
  
  // inline constexpr Derived() noexcept = default;
;



int main()

  Base<Derived> * b = static_cast<Base<Derived> *>(new Derived());
  return 0;


调用类模板成员函数后

#include<iostream>
using namespace std;

template<typename T>
struct Base 
    void interface() 
        static_cast<T*>(this)->implementation();	
    
    
    int get() const 
        return m_count;
    

    int m_count = 0;
;

struct Derived : Base<Derived> 
    void implementation() 
        m_count = 1;
    
;

int main() 
    Base<Derived>* b = new Derived;
    b->interface();
    cout << b->get() << endl;

    return 0;

insights.cpp

#include<iostream>
using namespace std;

template<typename T>
struct Base

  inline void interface()
  
    static_cast<T *>(this)->implementation();
  
  
  inline int get() const
  
    return this->m_count;
  
  
  int m_count = 0;
;

/* First instantiated from: insights.cpp:17 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct Base<Derived>

  inline void interface()
  
    static_cast<Derived *>(this)->implementation();
  
  
  inline int get() const
  
    return this->m_count;
  
  
  int m_count = 0;
  // inline constexpr Base() noexcept = default;
;

#endif


struct Derived : public Base<Derived>

  inline void implementation()
  
    /* static_cast<Base<Derived> *>(this)-> */ m_count = 1;
  
  
  // inline constexpr Derived() noexcept = default;
;



int main()

  Base<Derived> * b = static_cast<Base<Derived> *>(new Derived());
  b->interface();
  std::cout.operator<<(b->get()).operator<<(std::endl);
  return 0;


对比调用前后的insights.cpp代码可以发现,在实际调用b->interface()Base::interface() 并没有被实例化。所以虽然此时 Derived 还不是一个完整的类型,但并没有报错,你可以当作Base::interface() 里的代码不存在。在调用b->interface() 的时候,Derived 已经是一个完整类型了,此时再实例化类模板成员函数,就能调用 Derived::implementation()

可以发现,CRTP 利用继承 + 模板让基类在编译期就能知道派生类的信息,在原来的动态多态中需要通过虚函数查找虚表来获取信息,这就实现了静态多态。

#include<iostream>
using namespace std;

template<typename T>
struct Base 
    void interface() 
        static_cast<T*>(this)->implementation();	
    
    
    int get() const 
        return m_count;
    

    int m_count = 0;
;

struct Derived1 : Base<Derived1> 
    void implementation() 
        m_count = 1;
    
;

struct Derived2 : Base<Derived2> 
    void implementation() 
        m_count = 2;
    
;

int main() 
    Base<Derived1>* b1 = new Derived1;
    Base<Derived2>* b2 = new Derived2;
    b1->interface();
    cout << b1->get() << endl;
    b2->interface();
    cout << b2->get() << endl;

    return 0;

2.1.6动态多态与 CRTP 的对比

动态多态通过虚函数来实现,在性能上存在以下缺陷

  • 查找虚表需要一定时间(影响没那么大)
  • 难以被内联或优化(主要影响)

使用 Quick C++ Bench 进行基准测试,使用 Clang15.0C++20 编译,分别测试不同优化等级下的效果
代码来自:https://github.com/PacktPublishing/Hands-On-Design-Patterns-with-CPP/blob/master/Chapter08/function_call.C

#include <stdlib.h>

#include "benchmark/benchmark.h"

#define REPEAT2(x) x x
#define REPEAT4(x) REPEAT2(x) REPEAT2(x)
#define REPEAT8(x) REPEAT4(x) REPEAT4(x)
#define REPEAT16(x) REPEAT8(x) REPEAT8(x)
#define REPEAT32(x) REPEAT16(x) REPEAT16(x)
#define REPEAT(x) REPEAT32(x)

namespace no_polymorphism 
class A 
    public:
    A() : i_(0) 
    void f(int i)  i_ += i; 
    int get() const  return i_;以上是关于浅谈 CRTP:奇异递归模板模式的主要内容,如果未能解决你的问题,请参考以下文章

奇异递归模版模式不“奇异”

什么是奇怪的重复模板模式(CRTP)?

使用带有附加类型参数的奇怪重复模板模式 (CRTP)

使用奇怪的重复模板模式 (CRTP) 在抽象基类中实现赋值运算符

CRTP 特征仅适用于模板派生类

C++ 静态多态性 (CRTP) 和使用派生类的 typedef