前言
上篇文章中,我们探索了单个多态对象(没有继承)的虚函数表中的条目及它们的作用。本文继续探究普通单继承下的虚函数表。
本节示例代码如下:
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。
转化成图,如下:
上篇文章介绍过的内容不再重复,这里着重介绍以下几点:
- 因为含有纯虚函数的类不能实例化,自然也不存在析构,因此两个析构函数的地址都是0。
- 虚函数地址在虚函数表中的顺序与它们在类中的声明顺序一致,本例中,先是constructor,接着是Base::zoo(),最后是纯虚函数Base::foo()。读者可以调整这些函数的声明顺序,然后观察虚函数表的变化。
- __cxa_pure_virtual是一个错误处理函数,当调到纯虚函数时,实际上会执行这个函数,该函数最终会 std::abort() (source code)。什么时候会出现这种情况呢?这篇文章讲得很透彻,在下就不班门弄斧了。
Derived类虚函数布局
着重介绍以下几点。
合并的虚函数表
因为只有一个基类,且不是虚基类,因此基类子对象和派生类共用一个虚函数表。对于某个条目,如果派生类有自己的实现(比如typeinfo、override的虚函数等),那么就采用派生类的版本,否则,采用基类的版本。对于派生类新增的虚函数,按声明顺序依次排在最后面。如上图所示。
__si_class_type_info
和之前不同的是,这里type_info指针指向了 __si_class_type_info 对象。该类继承自上篇文章提到的 __class_type_info ,源码位于cxxabi.h,Itanium 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 toabi::__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 中发挥着重要作用。
由于在下才疏学浅,能力有限,错误疏漏之处在所难免,恳请广大读者批评指正,您的批评是在下前进的不竭动力。