CRTP避坑实践

CRTP避坑实践

>关注公众号【高性能架构探索】,第一时间获取最新文章;公众号内回复【pdf】,免费获取经典书籍

你好,我是雨乐!

在上一篇文章惯用法之CRTP(如果不了解什么是CRTP,请先阅读该篇文章😁)一文中,介绍了CRTP的基本原理。今天借助本文,总结下在开发过程中,使用CRTP遇到的坑。

容器存储

CRTP技术原理是调用派生类中的成员函数。但是,问题在于Base类实际上是一个模板类,而不是一个实际的类。因此,如果存在名为Derived和Derived1的派生类,则基类模板初始化将具有不同的类型。这是因为,Base类将派生自不同的特化,即 Base,代码如下:

#include <iostream>
#include <string>

template <typename T>
class Base{
 public:
  void interface(){
    static_cast<T*>(this)->imp();
  }
  void imp(){
    std::cout << "in Base::imp" << std::endl;
  }
};

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;
  }
};

int main() {
  Base<Derived> *b = new Derived;
  Base<Derived> *b1 = new Derived1;
  auto vec = {d, d1}; // 出错

  return 0;
}

在上述示例中,程序会输出如下:

In function ‘int main()’:
test.cc:39:20: error: unable to deduce ‘std::initializer_list<_Tp>’ from ‘{d, d1}’
auto vec = {d, d1};

从上面内容可以看出,vec类型推导失败,这是因为d和d1属于不同的类型,因此不能将CRTP对象或者指针放入容器中

堆栈溢出

首先,我们看一个例子:

#include <iostream>
#include <typeinfo>
#include <sys/time.h>

template<typename T>
class Base {
 public:
  void PrintType() {
    T &t = static_cast<T&>(*this);

    t.PrintType();
  }
};

class Derived : public Base<Derived> {
  // 此处没有实现PrintType()函数
};

int main() {
  Derived d;
  d.PrintType();

  return 0;
}

编译并运行之后,输出如下:

Segmentation fault

是不是感觉很奇怪,单分析代码,没看出什么问题来,于是借助gdb来进行分析,如下:

#124 0x00000000004006c4 in Base<Derived>::PrintType (this=0x7fffffffe38f)
    at crtp.cc:11
#125 0x00000000004006c4 in Base<Derived>::PrintType (this=0x7fffffffe38f)
    at crtp.cc:11
#126 0x00000000004006c4 in Base<Derived>::PrintType (this=0x7fffffffe38f)
    at crtp.cc:11
#127 0x00000000004006c4 in Base<Derived>::PrintType (this=0x7fffffffe38f)
    at crtp.cc:11
#128 0x00000000004006c4 in Base<Derived>::PrintType (this=0x7fffffffe38f)
    at crtp.cc:11
#129 0x00000000004006c4 in Base<Derived>::PrintType (this=0x7fffffffe38f)
    at crtp.cc:11
#130 0x00000000004006c4 in Base<Derived>::PrintType (this=0x7fffffffe38f)
    at crtp.cc:11
#131 0x00000000004006c4 in Base<Derived>::PrintType (this=0x7fffffffe38f)
    at crtp.cc:11

从上述gdb的分析结果看出,重复执行crtp.cc中第11,即递归调用t.PrintType()。那么为什么会出现这种递归调用这种现象呢?

在上一篇文章中,有提到,如果派生类没有实现某个基类中定义的函数,那么调用的是基类的函数。听起来比较绕口,我们以上述例子为例进行分析:

  • 在Base类中,定义了一个函数PrintType(),在该函数中通过state_cast转换后,调用PrintType()函数。
  • 派生类中没有实现PrintType()函数
  • 因为派生类中没有实现PrintType()函数,所以在基类进行调用的时候,仍然调用的是基类的PrintType()函数

正是因为以上几点,所以才导致了这种递归调用引起的堆栈溢出

那么,如何避免此类问题呢?可以使用下述方式实现:

#include <iostream>
#include <typeinfo>
#include <sys/time.h>

template<typename T>
class Base {
 public:
  void PrintType() {
    T &t = static_cast<T&>(*this);

    t.PrintTypeImpl();
  }

  void PrintTypeImpl() {}
};

class Derived : public Base<Derived> {
  // 此处没有实现PrintType()函数
};

int main() {
  Derived d;

  d.PrintType();

  return 0;
}

在上述方案中,在基类中重新定义了另外一个函数PrintTypeImpl(),这样在调用PrintType()的时候,如果派生类中没有实现PrintTypeImpl()函数,则会调用基类的PrintTypeImpl()函数,这样就避免了因为递归调用而导致的堆栈溢出问题。

手滑笔误

CRTP可以带来性能上的好处,但前提是我们写的代码真的遵守了那个规范。要是我们因为笔误写错了代码了呢?比如这样:

class Derived1 : public Base<Derived> { //此处有笔误
};

按照CRTP的要求,class Derived1 应该继承的是 Base<Derived1>。如果笔误写成上述这样,在基类 Base() 通过 static_cast 之后有可能有不预期行为发生的。

为了尽量将上述笔误尽可能早的暴露出来,我们可以使用下面这张方式:根据继承规则,派生类初始化时一定会先调用基底类的构造函数,所以我们就将基类的构造函数声明为private,并且,利用 friend 修饰符的特点,即只有继承的子类 T 可以访问这个私有构造函数。其它的类如果想要访问这个私有构造函数,就会在编译期报错,如此做法,可以将问题暴露在编译阶段。

即将基类Base重新定义为如下格式:

template<typename T>
class Base {
 public:
  virtual void PrintType() const {
    std::cout << typeid(*this).name() << std::endl;
  }
  private:
    Base() = default;
    friend T;
};

经过上述修改,Base中只能Derived类访问Base类的构造函数,而Derived1是不能访问Base类构造函数的,因此在编译阶段失败。

如上代码,编译的时候,会提示如下报错:

test.cc: In function ‘int main()’:
test.cc:39:12: error: use of deleted function ‘Derived1::Derived1()’
   Derived1 d1;
            ^
test.cc:24:7: note: ‘Derived1::Derived1()’ is implicitly deleted because the default definition would be ill-formed:
 class Derived1 : public Base<Derived> {
       ^
test.cc:12:5: error: ‘Base<T>::Base() [with T = Derived]’ is private
     Base() = default;
     ^
test.cc:24:7: error: within this context
 class Derived1 : public Base<Derived> {

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/1181

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(1)
上一篇 2022年7月4日 下午3:17
下一篇 2022年7月11日 上午9:22

相关推荐