前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CRTP避坑实践

CRTP避坑实践

作者头像
高性能架构探索
发布2022-08-25 16:28:16
7540
发布2022-08-25 16:28:16
举报
文章被收录于专栏:技术随笔心得

你好,我是雨乐!

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

容器存储

CRTP技术因为其性能优越,实现简单,在工程应用中非常广泛。实际上,相对于普通的虚函数,其具有一定的局限性。问题在于Base类实际上是一个模板类,而不是一个实际的类。因此,如果存在名为Derived和Derived1的派生类,则基类模板初始化将具有不同的类型。这是因为,Base类将派生自不同的特化,即 Base,代码如下:

代码语言:javascript
复制
#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;
}

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

代码语言:javascript
复制
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对象或者指针放入容器中

堆栈溢出

首先,我们看一个例子:

代码语言:javascript
复制
#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;
}

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

代码语言:javascript
复制
Segmentation fault

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

代码语言:javascript
复制
#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()函数

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

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

代码语言:javascript
复制
template<typename T>
class Base {
 public:
  void PrintType() {
    T &t = static_cast<T&>(*this);

    t.PrintTypeImpl();
  }

  void PrintTypeImpl() {}
};

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

int main() {
  Derived d;

  d.PrintType();

  return 0;
}

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

手滑笔误

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

代码语言:javascript
复制
class Derived1 : public Base<Derived> { //此处有笔误
};

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

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

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

代码语言:javascript
复制
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类构造函数的,因此在编译阶段失败。

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

代码语言:javascript
复制
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> {

好了,今天的文章就到这,我们下期见!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-07-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 高性能架构探索 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 容器存储
  • 堆栈溢出
  • 手滑笔误
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档