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

前言

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

本节示例代码如下:

 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::zoon"; 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::funn"; 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 中发挥着重要作用。

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

发表评论

您必须 [ 登录 ] 才能发表留言!

相关文章