前言
上篇文章中,我们探索了单个多态对象(没有继承)的虚函数表中的条目及它们的作用。本文继续探究普通单继承下的虚函数表。
本节示例代码如下:
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。

转化成图,如下:

上篇文章介绍过的内容不再重复,这里着重介绍以下几点:
- 因为含有纯虚函数的类不能实例化,自然也不存在析构,因此两个析构函数的地址都是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 中发挥着重要作用。
由于在下才疏学浅,能力有限,错误疏漏之处在所难免,恳请广大读者批评指正,您的批评是在下前进的不竭动力。