虚函数表里有什么?(二)——普通单继承下的虚函数表

前言

上篇文章中,我们探索了单个多态对象(没有继承)的虚函数表中的条目及它们的作用。本文继续探究普通单继承下的虚函数表。

本节示例代码如下:

复制代码
 1 #include <iostream>
 2 #include <typeinfo>
 3 
 4 class Base
 5 {
 6 public:
 7     Base() {}
 8     virtual ~Base() {}
 9     virtual void zoo()
10     {
11         std::cout << "Base::zoo\n";
12     }
13     virtual void foo() = 0;
14 private:
15     int b_num = 100;
16 };
17 
18 class Derived : public Base
19 {
20 public:
21     Derived() {}
22     ~Derived() {}
23     virtual void fun()
24     {
25         std::cout << "Derived::fun\n";
26     }
27     void foo() override
28     {
29         std::cout << "my num is: " << d_num << '\n';
30     }
31 private:
32     int d_num = 200;
33 };
34 
35 int main(int argc, char *argv[])
36 {
37     std::cout <<sizeof(Derived) << '\n';
38     Base *p = new Derived;
39     const std::type_info &info = typeid(*p);
40     std::cout << info.name() << '\n';
41     delete p;
42     return 0;
43 }

Base类虚函数布局

Base类有纯虚函数,不能实例化,那我们如何查看它的vtable呢?一种方式是通过Compiler Explorer,另一种是通过GDB。

转化成图,如下:

上篇文章介绍过的内容不再重复,这里着重介绍以下几点:

  1. 因为含有纯虚函数的类不能实例化,自然也不存在析构,因此两个析构函数的地址都是0。
  2. 虚函数地址在虚函数表中的顺序与它们在类中的声明顺序一致,本例中,先是constructor,接着是Base::zoo(),最后是纯虚函数Base::foo()。读者可以调整这些函数的声明顺序,然后观察虚函数表的变化。
  3. __cxa_pure_virtual是一个错误处理函数,当调到纯虚函数时,实际上会执行这个函数,该函数最终会 std::abort() (source code)。什么时候会出现这种情况呢?这篇文章讲得很透彻,在下就不班门弄斧了。

Derived类虚函数布局

着重介绍以下几点。

合并的虚函数表

因为只有一个基类,且不是虚基类,因此基类子对象和派生类共用一个虚函数表。对于某个条目,如果派生类有自己的实现(比如typeinfo、override的虚函数等),那么就采用派生类的版本,否则,采用基类的版本。对于派生类新增的虚函数,按声明顺序依次排在最后面。如上图所示。

__si_class_type_info

和之前不同的是,这里type_info指针指向了 __si_class_type_info 对象。该类继承自上篇文章提到的 __class_type_info ,源码位于cxxabi.hItanium C++ ABI的解释是:

For classes containing only a single, public, non-virtual base at offset zero (i.e. the derived class is dynamic iff the base is), class abi::__si_class_type_info is used. It adds to abi::__class_type_info a single member pointing to the type_info structure for the base type, declared "__class_type_info const *__base_type".

即,使用 __si_class_type_info 的条件是:1)单一继承;2)public继承;3)不是虚继承;4)基类对象是polymorphic object(这个概念在上篇文章介绍过)。

相比于 __class_type_info , __si_class_type_info 多了一个指向直接基类typeinfo信息的指针 __base_type 。那么, __base_type 有什么用呢?

用途一:异常捕获时的类型匹配

对于本文示例,执行下面的代码时(需要将 Base::foo 改为非纯虚函数),

复制代码
try {
    throw Derived();
} catch (const Base& b) {
    b.foo();
}

在catch实现的核心函数 __do_catch 里(source code),会判断抛出的异常类型和捕获的异常类型是否匹配。

复制代码
bool __class_type_info::
__do_catch (const type_info *thr_type,
            void **thr_obj,
            unsigned outer) const
{
  // 这里==调用的是基类std::type_info的operator==, 本质上就是比较typeinfo name这一字符串常量
  if (*this == *thr_type)
    return true;
  if (outer >= 4)
    // Neither `A' nor `A *'.
    return false;
  // 如果不匹配,就看thr_type的上层类型是否匹配
  return thr_type->__do_upcast (this, thr_obj);
}

本例中, thr_type 是指向typeinfo for Derived,即 __si_class_type_info 对象的指针, this 指针是指向typeinfo for Base,即 __class_type_info 对象的指针。 std::type_info::operator== 的实现代码见这里

__si_class_type_info::__do_upcast里,如果当前类型(这里是Derived类型)和要捕获的目标类型(这里是Base类型)不相同,就调用 __base_type->__do_upcast ,去看基类的类型和要捕获的类型是否相同。如此这般,直到匹配或者upcast到最"祖先"的类型。

复制代码
bool __si_class_type_info::
__do_upcast (const __class_type_info *dst, const void *obj_ptr,
             __upcast_result &__restrict result) const
{
  // 如果当前类型和dst(即要捕获的类型)相同,返回true
  if (__class_type_info::__do_upcast (dst, obj_ptr, result))
    return true;

  // 否则看基类类型是否和dst相同
  return __base_type->__do_upcast (dst, obj_ptr, result);
}

bool __class_type_info::
__do_upcast (const __class_type_info *dst, const void *obj,
             __upcast_result &__restrict result) const
{
  if (*this == *dst) // 相同就返回true
    {
      result.dst_ptr = obj;
      result.base_type = nonvirtual_base_type;
      result.part2dst = __contained_public;
      return true;
    }
  return false;
}

用途二:dynamic_cast中的类型回溯

需要注意的是,如果是向上转换(upcast),如下,

复制代码
Derived *pd = new Derived;
Basel *pb = dynamic_cast<Base *>(pd);

gcc编译器通常会优化成从派生类对象到基类子对象的简单指针移动,不会去调用 dynamic_cast 操作符(是的,它是operator,不是函数)的底层实现 __dynamic_cast (这是函数,是gcc对 dynamic_cast 的实现),即使 -O0 优化级别也是如此。因此,我们重点关注向下转换(downcast)的情形。

复制代码
struct A { virtual ~A(){} };
struct B : public A {};
struct C : public B {};
struct D : public C {};

int main() {
  A *pa = new D;
  B *pb = dynamic_cast<B*>(pa);
  int ret = nullptr == pb ? -1 : 0;
  delete pa;
  return ret;
}

这里,是从基类A到派生类B的向下转换, dynamic_cast 会检查是否可以转换,因为 pa 实际指向的最派生类D的实例,因此从本质上讲还是完整对象到基类子对象的转换,因此,最终转换是成功的。那么, dynamic_cast 是如何做到这一点的呢?让我们从核心实现 __dynamic_cast 开始。

复制代码
extern "C" void *
__dynamic_cast (const void *src_ptr,    // object started from
                const __class_type_info *src_type, // type of the starting object
                const __class_type_info *dst_type, // desired target type
                ptrdiff_t src2dst) // how src and dst are related
  {
  if (__builtin_expect(!src_ptr, 0)) // 如果源指针是空,直接返回空
    return NULL; // Handle precondition violations gracefully.
  // 这里就是利用虚函数表里的top_offset和typeinfo信息,找到完整对象(也
  // 就是最派生类对象)的指针和类型信息
  const void *vtable = *static_cast <const void *const *> (src_ptr);
  const vtable_prefix *prefix =
    (adjust_pointer <vtable_prefix>
     (vtable,  -ptrdiff_t (offsetof (vtable_prefix, origin))));
  const void *whole_ptr =
      adjust_pointer <void> (src_ptr, prefix->whole_object);
  const __class_type_info *whole_type = prefix->whole_type;
  __class_type_info::__dyncast_result result; // 构造一个result,存放__do_cast的结果

  // 这里省略一些与本主题无关的校验代码

  // 从完整对象的类型(本例是D)向上回溯,寻找目标类型dst_type(本例是B)
  whole_type->__do_dyncast (src2dst, __class_type_info::__contained_public,
                            dst_type, whole_ptr, src_type, src_ptr, result);

  // 根据result确定返回结果,代码先省略
  }

__dynamic_cast 会根据待转换对象的指针和类型信息,通过虚函数表中的top_offset和typeinfo,拿到最派生对象的指针和类型信息,然后层层回溯,看看能不能回溯到目标类型。对本例来说,就是先从A类型得到最派生类型D,然后从D逐级回溯,D-->C-->B。

复制代码
bool 
__do_dyncast (ptrdiff_t src2dst,
              __sub_kind access_path,
              const __class_type_info *dst_type,
              const void *obj_ptr,
              const __class_type_info *src_type,
              const void *src_ptr,
              __dyncast_result &__restrict result) const
{
  if (*this == *dst_type)
    {
      result.dst_ptr = obj_ptr; // 这里其实就是dynamic_cast的返回值
      // 还会设置result的其它字段,与本主题无关,先略过
      return false; // false的意思是:不用再回溯了
    }
  if (obj_ptr == src_ptr && *this == *src_type) // 先略过
    {
      // 省略一些代码
      return false;
    }
  // 如果当前类型不匹配,就回溯到上一层
  return __base_type->__do_dyncast (src2dst, access_path, dst_type, obj_ptr,
                             src_type, src_ptr, result);
}

因为过分深入细节会偏离主题,所以本文仅点到为止,等到讲述完虚函数表相关的内容,后面会专门拿出一篇文章,结合实例,讲解 __dynamic_cast 的实现细节,帮助读者把之前的知识融会贯通。

gcc源码位置:__dynamic_cast__si_class_type_info::__do_dynamic

用途三:dynamic_cast中寻找public基类

如果通过找到了转换目标的地址,但是却不能确定 src_type 是不是 dst_type 的public基类(如果不是,转换就会失败,返回空指针),因此需要从 dst_type 向上回溯,看能不能找出到 src_type 的public路径。

复制代码
// __dynamic_cast中的逻辑
if (result.dst2src == __class_type_info::__unknown)
    result.dst2src = dst_type->__find_public_src (src2dst, result.dst_ptr,
                                                  src_type, src_ptr);

__find_public_src 是 __class_type_info 的成员函数,在tinfo.h中定义。

复制代码
inline __class_type_info::__sub_kind __class_type_info::
__find_public_src (ptrdiff_t src2dst,
                   const void *obj_ptr,
                   const __class_type_info *src_type,
                   const void *src_ptr) const
{
  if (src2dst >= 0) // 若大于0,src是dst的基类子对象,接下来看加上偏移量后指针是否匹配
    return adjust_pointer <void> (obj_ptr, src2dst) == src_ptr
            ? __contained_public : __not_contained;
  if (src2dst == -2) // 等于-2表示:src is not a public base of dst
    return __not_contained;
  // 其余情况需要调用__do_find_public_src逐级回溯
  return __do_find_public_src (src2dst, obj_ptr, src_type, src_ptr);
}

__si_class_type_info::__do_find_public_src 会逐级向上回溯。

复制代码
__class_type_info::__sub_kind __si_class_type_info::
__do_find_public_src (ptrdiff_t src2dst,
                      const void *obj_ptr,
                      const __class_type_info *src_type,
                      const void *src_ptr) const
{
  if (src_ptr == obj_ptr && *this == *src_type)
    return __contained_public;
  return __base_type->__do_find_public_src (src2dst, obj_ptr, src_type, src_ptr);
}

那么,什么情况下需要逐级寻找public base呢?比如说下面的代码:

复制代码
class Base { public: virtual ~Base() {} };          
class Middle : public virtual Base {};             
class Derived : public Middle {};                 

int main() {
    Base* base_ptr = new Derived;                 
    Derived* derived_ptr = dynamic_cast<Derived*>(base_ptr);
    int ret = nullptr == derived_ptr ? -1 : 0;
    delete base_ptr;
    return ret;
}

因为这里继承关系比较复杂(涉及到虚拟继承),所以 __do_dyncast 不能确定 dst2src 是什么,需要再次回溯。由于虚拟继承超出了本文的讨论范围,因此暂不深入分析,留待后序文章探讨。

总结

  • 在vtable中,纯虚函数对应 __cxa_pure_virtual 这个错误处理函数,该函数的本质是调用 about() ,即,如果调用纯虚函数,会导致程序奔溃。
  • 如果一个类只有一个基类,并且这个基类是public的、非虚的、多态的(含有虚函数),那么,派生类对象和基类子对象公用一个vtable,对于某个条目,如果派生类有自己的实现,那么就采用派生类的版本,否则,采用基类的版本。对于派生类新增的虚函数,按声明顺序依次排在最后面。
  • 对于满足上述条件的派生类,它对应的typeinfo类型是 __si_class_type_info ,该类是 __class_type_info 的派生类,含有一个指向基类typeinfo的指针 __base_type ,依靠该指针,可以从派生类类型到基类类型进行逐层回溯,这在异常捕获、 dynamic_cast 中发挥着重要作用。

由于在下才疏学浅,能力有限,错误疏漏之处在所难免,恳请广大读者批评指正,您的批评是在下前进的不竭动力。

相关推荐
longlong int1 小时前
【每日算法】Day 17-1:位图(Bitmap)——十亿级数据去重与快速检索的终极方案(C++实现)
开发语言·c++·算法
myloveasuka2 小时前
[Linux]进程与PCB的关系,进程的基本操作
linux·c语言·c++
歪~~2 小时前
KMP算法
数据结构·c++·算法
夏天的阳光吖3 小时前
C++蓝桥杯实训篇(二)
开发语言·c++·蓝桥杯
梁下轻语的秋缘3 小时前
每日c/c++题 备战蓝桥杯(小球反弹)[运动分解求解,最大公约数gcd]
c语言·c++·学习·算法·数学建模·蓝桥杯
SiMmming3 小时前
【算法竞赛】状态压缩型背包问题经典应用(蓝桥杯2019A4分糖果)
c++·经验分享·算法·职场和发展·蓝桥杯·动态规划
DexterYttt3 小时前
AT_abc212_d [ABC212D] Querying Multiset
数据结构·c++·算法·优先队列
Tadecanlan4 小时前
[C++面试] explicit面试8问 —— 较难,可简单了解即可
开发语言·c++
the_nov4 小时前
9.进程信号
linux·c++
哒宰的自我修养4 小时前
0.DJI-PSDK开发准备及资料说明(基于DJI经纬M300RTK和M350RTK无人机上使用)
c++·学习·无人机