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


  同学们是否有听说过奇异递归模版模式(CRTP)?听说过的同学大致也知道其代码编写格式是怎么样的?但是,同学们是否有弄清楚过其是怎么达到这种效果的?接下来就简单聊聊!

一、奇异递归模板模式

  下面是奇异递归模板模式的一般编写格式:

 1 template
 2 class Base
 3 {
 4 public:
 5     Base()
 6     {
 7         std::cout << "Base::Base<" << typeid(T).name() << "> ctor\n";
 8     }
 9     ~Base()
10     {
11         std::cout << "~Base::Base<" << typeid(T).name() << "> dtor\n";
12     }
13     void Interface()
14     {
15         std::cout << "Base::Interface()\n";
16         static_cast(this)->Implement1();
17     }
18 };
19 
20 
21 class D : public Base
22 {
23 public:
24     D()
25     {
26         std::cout << "D::D<" << typeid(this).name() << "> ctor\n";
27     }
28     ~D()
29     {
30         std::cout << "~D::D<" << typeid(this).name() << "> dtor\n";
31     }
32 
33     void Implement()
34     {
35         std::cout << "D::Implement()\n";
36     }
37 };

  有一个基类模板,子类继承该基类时,使用子类实例化基类。起到调用基类接口实际上动态调用子类方法的作用,又被称之为“静态多态”(和重载不同)。调用方法如下:

1 int main()
2 {
3     D b;
4     b.Interface();
5     return 0;
6 }

  运行结果:

     

  要想弄清楚奇异递归模板模式如何达到这种效果,得先弄清楚两个基础问题:

  1. 类模板或函数模板什么时候被编译?
  2. std::static_cast能否向上转换?

二、类模板或函数模板什么时候被编译?

  回答类模板或函数模板什么时候被编译前,先问自己:函数模板和模板函数,类模板和模板类的区别是什么。思考下... ...。其实类模板或函数模板,就是带有模板类型参数的类或函数,比如上面的奇异递归模板就是一个类模板;模板类或函数,就是实例化模板的类或函数,比如子类继承基类就是模板类,也叫做模板实例化。我们编写的类模板或函数模板,在没有实例化前,是不会被编译器编译。怎么验证呢?很简单,没有实例化上面的奇异递归模板时,我随便写过不存在的调用函数,编译不会出现错误。如下类模板:

 1 template
 2 class Base
 3 {
 4 public:
 5     Base()
 6     {
 7         std::cout << "Base::Base<" << typeid(T).name() << "> ctor\n";
 8     }
 9     ~Base()
10     {
11         std::cout << "~Base::Base<" << typeid(T).name() << "> dtor\n";
12     }
13     void Interface()
14     {
15         std::cout << "Base::Interface()\n";
16         static_cast(this)->Implement1(); // 不存在的函数
17         auto value = T::value;               // 不存在的值
18     }
19 };

  如果实例化类模板,那么就会编译报错:

      

  预处理后的预处理文件还是没有对类模板进行类型推导:

  

  所以,通过上面的验证,类模板或函数模板必须要实例化,然后编译器才会在编译阶段的时候进行类型推导并编译出obj文件。

三、std::static_cast能否向上转换?

  先按照老规则,官方文档链接:static_cast conversion - cppreference.com。

  第9种情况说明了std::static_cast可以向上转换,但也强调,这种转换并不进行检查以确保转换后对象的成员是否存在。下面可以通过代码验证:

 1 class B {
 2 public:
 3     B()
 4         : _b(1)
 5     {
 6         std::cout << "B::B() ctor\n";
 7     }
 8     virtual ~B()
 9     {
10         std::cout << "B::~B() dctor\n";
11     }
12 
13 private:
14     int _b;
15 };
16 
17 class D : public B
18 {
19 public:
20     D()
21         : _d(2)
22     {
23         std::cout << "D::D() ctor\n";
24     }
25     ~D()
26     {
27         std::cout << "D::~D() ctor\n";
28     }
29     void Func()
30     {
31         //this->_d = 2;
32         std::cout << "D::Func() " << _d << std::endl;
33     }
34 private:
35     int _d;
36 };
37 
38 int main()
39 {
40     auto b = new B();
41     auto d = static_cast(b);
42     d->Func();
43     return 0;
44 }

      

  虽然转换后的子类可以正常调用子类函数,但是子类成员变量是随机值,这个也验证了官方文档中第9条转换规则中强调的问题。关于奇异递归模板模式的作用和使用问题,在实际项目中没有使用过,但是在boost等一些开源库中有涉及到它,感兴趣的同学可以访问下面的参考链接地址,进一步了解和学习奇异递归模板模式。

参考:The Curiously Recurring Template Pattern (CRTP) - Fluent C++ (fluentcpp.com)