C++:从技术实现角度聊聊RTTI

第一次接触rtti,是在这本书中,当时对这块的理解比较浅,可能因为知识积累不足吧。后面在工作中用到的越来越多,也逐渐加深了对其认识,但一直没有一个系统的认知,所以抽出一段时间,把这块内容整理下。
背景 rtti的英文全称是runtime type identification,中文称为运行时类型识别,它指的是程序在运行的时候才确定需要用到的对象是什么类型的。用于在运行时(而不是编译时)获取有关对象的信息。
在c++中,由于存在多态行为,基类指针或者引用指向一个派生类,而其指向的真正类型,在编译阶段是无法知道的:
base *b = new derived;base &b1 = *b; 在上述代码中,如果想知道b的具体类型,只能通过其他方式,而rtti正是为了解决此问题而诞生,也就是说在运行时,rtti可以通过特有的方式来告诉调用方其所调用的对象具体信息,一般有如下几种:
• typeid操作符
• type_info类
• dynamic_cast操作符
typeid 和 type_info typeid是c++的关键字之一,等同于sizeof这类的操作符。用来获取类型、变量、表达式的类型信息,适用于c++基础类型、内置类、用户自定义类、模板类等。有如下两种形式:
• typeid(type)
• typeid(expr)
用法如下:
#include #include #include class base {public:  virtual float f() {     return 1.0;  }    virtual ~base() {}};class derived : public base {};int main() {  base* p = new derived;  base& r = *p;  assert(typeid(p) == typeid(base*));  assert(typeid(p) != typeid(derived*));  assert(typeid(r.f()) == typeid(float));    const char *name = typeid(p).name();    std::cout << name << std::endl;  return 0;}  返回值 在上面的例子中,用到了了 typeid(xxx).name() ,通过其名称可以看出name()函数返回的是具体类型的变量名称(以字符串的方式),那么typeid()的类型又是什么?
在翻阅了cppreference之后了解到,typeid操作符的结果是名为type_info的标准库类型的对象的引用(在头文件中定义),或者说typeid表达式的类型是const std::type_info& 。
iso c++标准并没有对type_info有明确的要求,仅仅要求必须有以下几个行为接口:
• t1 == t2 // 如果两个对象t1和t2类型相同,则返回true;否则返回false
• t1 != t2 // 如果两个对象t1和t2类型不同,则返回true;否则返回false
• t.name() // 返回类型的c-style字符串
• t1.before(t2) // 抱歉,我没用过
正是因为标准对type_info做了有限的规定,这就使得每个编译器厂商对type_info类的实现均不相同,从而使得函数功能也不尽相同。以常用的函数typeid().name()举例,int和base(自定义类)在vs下输出分别为int和base,而在gcc编译器下,其输出为i和4base,又比如typeid(std::vector).name()在gcc下输出为st6vectoriisaiiee,这是因为编译期对名称进行了mangle,如果我们想得到跟vs下一样结果的话,可以采用如下方式:
#include #include #include #include #include #include std::string demangle(const char* name) {  int status = -4;  std::unique_ptr res {         abi::__cxa_demangle(name, null, null, &status),                 std::free         };  return (status==0) ? res.get() : name ;}int main() {  std::vector v;  std::cout << before:  << typeid(v).name() <<  after:  << demangle(typeid(v).name()) << std::endl;  return 0;} 输出如下:
before: st6vectoriisaiiee after: std::vector 下面是gcc编译器对type_info类的定义(仅抽取了声明部分),如果有兴趣的读者可以点击链接自行阅读:
class type_info { public:  virtual ~type_info();  const char* name() const;  bool before(const type_info& __arg) const;  bool operator==(const type_info& __arg) const;  bool before(const type_info& __arg) const;  bool operator==(const type_info& __arg) const;  bool before(const type_info& __arg) const;  bool operator==(const type_info& __arg) const;  bool operator!=(const type_info& __arg) const;  size_t hash_code() const throw();  virtual bool __is_pointer_p() const;  virtual bool __is_function_p() const;  virtual bool __do_catch(const type_info *__thr_type, void **__thr_obj,                 unsigned __outer) const;  virtual bool __do_upcast(const __cxxabiv1::__class_type_info *__target,                  void **__obj_ptr) const; protected:  const char *__name;  explicit type_info(const char *__n): __name(__n) { } private:  type_info& operator=(const type_info&);  type_info(const type_info&);}; 从上述定义可以看出,其析构函数声明为virtual,至少可以说明其存在子对象,那么子对象又是如何被使用的呢?
其实,type_info可以当做一个接口类(通过调用typeid()获取type_info对象,实际上返回的是一个指向子类对象的type_info引用),其有多个子类,对于有虚函数的类来说,在虚函数表中有一个slot专门用来存储该对象的信息,这块内容在文章后面将有详细说明。
实现 在前面有提到,typeid()会返回一个const std::type_info&对象,其中存储这对象的基本信息,那么如果其类型对象为多态和非多态时候,其又有什么区别呢?
如果类型对象至少包含一个虚函数,那么typeid操作符的类型是运行时的事情,也就是说在运行时才能获取到其真正的类型信息;否则,在编译期就能获取其具体类型,甚至在某些情况下,可以对typeid()的结果直接进行替换。
多态 多态,我们知道经常用于运行时,也就是说在运行时刻才会知道其指针或者引用指向的具体类型,如果要对一个包含虚函数的对象获取其类型信息(typeid),那么也是在运行时才能具体知道,举例如下:
#include #include class base{public:     virtual void fun() {}};class derived : public base{public:     void fun() {}};void fun(base *b) {  const std::type_info &info = typeid(b);}int main() {  base *b = new derived;  fun(b);    return 0;} 上述代码汇编后(只取了部分关键代码),如下所示:
fun(base*):        push    rbp        mov     rbp, rsp        mov     qword ptr [rbp-24], rdi        mov     qword ptr [rbp-8], offset flat:typeinfo for base*        pop     rbp        retvtable for derived:        .quad   0        .quad   typeinfo for derived        .quad   derived::fun()vtable for base:        .quad   0        .quad   typeinfo for base        .quad   base::fun()typeinfo name for base*:        .string p4basetypeinfo for base*:        .quad   vtable for __cxxabiv1::__pointer_type_info+16        .quad   typeinfo name for base*        .long   0        .zero   4        .quad   typeinfo for basetypeinfo name for derived:        .string 7derivedtypeinfo for derived:        .quad   vtable for __cxxabiv1::__si_class_type_info+16        .quad   typeinfo name for derived        .quad   typeinfo for basetypeinfo name for base:        .string 4basetypeinfo for base:        .quad   vtable for __cxxabiv1::__class_type_info+16        .quad   typeinfo name for base 首先,我们看fun()函数的汇编(fun(base*):处),在其中有一行offset flat:typeinfo for base* 代表获取base指针所指向对象的typeinfo。那么typeinfo又是如何获取的呢?
我们以base指针实际指向derived对象为例,vtable for derived:部分代表着derived类的虚函数表内容,其中有一行typeinfo for derived代表着derived类的typeinfo信息,而在该段中有一句typeinfo name for derived代表着该类的名称(7derived经过mangle之后,该句在上述代码中可以找到)。
综上内容,可以知道,对于存在虚函数的类来说,其对象的typeinfo信息存储在该类的虚函数表中。在运行时刻,根据指针的实际指向,获取其typeinfo()信息,从而进行相关操作。
其实,不难看出,上述汇编基本列出了类的对象布局,但仍然不是很清晰,gcc提供了一个参数 -fdump-class-hierarchy ,可以输出类的布局信息,仍然以上述代码为例,其布局信息如下:
vtable for basebase: 3u entries0     (int (*)(...))08     (int (*)(...))(& _zti4base)16    (int (*)(...))base::funclass base   size=8 align=8   base size=8 base align=8base (0x0x7f59773402a0) 0 nearly-empty    vptr=((& base::_ztv4base) + 16u)vtable for derivedderived: 3u entries0     (int (*)(...))08     (int (*)(...))(& _zti7derived)16    (int (*)(...))derived::funclass derived   size=8 align=8   base size=8 base align=8derived (0x0x7f59773756e8) 0 nearly-empty    vptr=((& derived::_ztv7derived) + 16u)  base (0x0x7f5977340300) 0 nearly-empty      primary-for derived (0x0x7f59773756e8) 我们注意查看,以 _zti 开头的代表类型信息,也就是type info的意思(至于以_z的意思嘛,我理解的是编译器的行为),那么 _zti7derived 前面的_zti代表类型信息,而后面7代表类名(derived)的长度,最后面的代表类名。通过上面内存布局信息可以看出,在虚函数表中存在一项_zti7derived,其中存储着该对类的类型信息。
如果想要知道其具体名称,可以使用c++filt来查看,如下:
c++filt _zti7derivedtypeinfo for derived 非多态 代码如下:
#include #include #include class myclss {};int main() {  myclss s;  const std::type_info &info = typeid(s);    return 0;} 在上述代码中,实现了一个空类myclass,然后在main()中,获取该类对象的typeinfo,上述代码汇编如下:
main:        push    rbp        mov     rbp, rsp        mov     qword ptr [rbp-8], offset flat:typeinfo for myclss        mov     eax, 0        pop     rbp        rettypeinfo name for myclss:        .string 6myclsstypeinfo for myclss:        .quad   vtable for __cxxabiv1::__class_type_info+16        .quad   typeinfo name for myclss 我们注意下在源码中的第三行即const std::type_info &info = typeid(s);对应汇编的第三行即qword ptr [rbp-8], offset flat:typeinfo for myclss,从而可以看出,在编译期,编译器已经知道了对象的具体信息,进而可以在某些情况下,直接由编译器进行替换(比如typeinf().name()操作等)。
dynamic_cast 记得在几年前的一次面试中,面试官提了个问题,对于dynamic_cast,如果操作失败了会有什么行为?当时对这块理解的也不深,所以仅仅回答了:对于指针类型转换,如果失败,则返回null,而对于引用,转换失败就抛出bad_cast。
作为c++开发人员,基本都知道dynamic_cast是c++中几个常用的类型转换符之一,其通过类型信息(typeinfo)进行相对安全的类型转换,在转换时,会检查转换的src对象是否真的可以转换成dst类型。dynamic_cast转换符只能用于含有虚函数的类,因此其常常用于运行期,对于不包括虚函数的类,完全可以使用其它几个转换符在编译期进行转换。通常来说,其类型转换分为向上转换和向下转换两种,如下图所示:
实例代码如下:
#include #include class base1 {public:  void f0() {}  virtual void f1() {}  int a;};class base2 {public:  virtual void f2() {}  int b;};class derived : public base1, public base2 {public:  void d() {}  void f2() {}  // override base2::f2()  int c;};int main() {  derived *d = new derived;  base1 *b1 = new derived;  base2 *b2 = dynamic_cast(d); // upcasting 向上转换  derived *d1 = dynamic_cast(b1); // downcasting 向下转换    return 0;} 实现 通过查阅资料,发现dynamic_cast最终会调用libstdc++中的__dynamic_cast函数,所以曾经以为__dynamic_cast函数就是dynamic_cast的实现版本,但是通过对比参数,发现并非如此:
dynamic_cast(t); // 只有一个参数// __dynamic_cast声明__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 所以,有没有可能__dynamic_cast只是dynamic_cast的一个分支实现?
为了验证猜测,示例如下:
#include #include class base1 {public:  void f0() {}  virtual void f1() {}  int a;};class base2 {public:  virtual void f2() {}  int b;};class derived : public base1, public base2 {public:  void d() {}  void f2() {}  // override base2::f2()  int c;};template int checktype(t t) {  int n = 0;  if (dynamic_cast(t)) {    n |= 1;  }   if (dynamic_cast(t)) {    n |= 2;  }  if (dynamic_cast(t)) {    n |= 4;  }  return n;}int main() {  derived  *d  = new derived;  base1 *b1 = new base1;  base2 *b2 = new base2;  checktype(d);  checktype(b1);  checktype(b2);  return 0;} 既然本节内容是dynamic_cast,而只在 checktype() 函数中才有对dynamic_cast的调用,那么我们着重分析checktype函数。
首先,我们通过g++的命令-fdump-class-hierarchy获取其内存布局,derived内存布局如下(需要注意32 (int (*)(...))-16 和 base2 (0x0x7f7fbbe5b6c0) 16部分):
vtable for derivedderived: 7u entries0     (int (*)(...))08     (int (*)(...))(& _zti7derived)16    (int (*)(...))base1::f124    (int (*)(...))derived::f232    (int (*)(...))-1640    (int (*)(...))(& _zti7derived)48    (int (*)(...))derived::_zthn16_n7derived2f2evclass derived   size=32 align=8   base size=32 base align=8derived (0x0x7f7fbbf10c40) 0    vptr=((& derived::_ztv7derived) + 16u)  base1 (0x0x7f7fbbe5b660) 0      primary-for derived (0x0x7f7fbbf10c40)  base2 (0x0x7f7fbbe5b6c0) 16      vptr=((& derived::_ztv7derived) + 48u) 向上转换 在checktype(derived*)处,通过gdb进行分析,如下:
(gdb) disasdump of assembler code for function _z9checktypeip7derivedeit_:   0x00000000004009ce : push   %rbp   0x00000000004009cf : mov    %rsp,%rbp   0x00000000004009d2 : mov    %rdi,-0x18(%rbp)=> 0x00000000004009d6 : movl   $0x0,-0x4(%rbp)   0x00000000004009dd : cmpq   $0x0,-0x18(%rbp)   0x00000000004009e2 : je     0x4009e8    0x00000000004009e4 : orl    $0x1,-0x4(%rbp) ; if t != nullptr      0x00000000004009e8 : cmpq   $0x0,-0x18(%rbp)   0x00000000004009ed : je     0x4009f3    0x00000000004009ef : orl    $0x2,-0x4(%rbp) ; if t != nullptr      0x00000000004009f3 : cmpq   $0x0,-0x18(%rbp)   0x00000000004009f8 : je     0x400a0b    0x00000000004009fa : mov    -0x18(%rbp),%rax   0x00000000004009fe : add    $0x10,%rax   0x0000000000400a02 : test   %rax,%rax   0x0000000000400a05 : je     0x400a0b    0x0000000000400a07 : orl    $0x4,-0x4(%rbp) ; if t != nullptr && t + 0x10 != nullptr   0x0000000000400a0b : mov    -0x4(%rbp),%eax   0x0000000000400a0e : pop    %rbp   0x0000000000400a0f : retqend of assembler dump. 为了便于理解,在上述代码关键部分加上了注释.
我们注意到,在上述汇编代码中,没有找到外部函数调用(__dynamic_cast),而仅仅是一些常用的跳转和比较指令。其中,前两条orl指令的执行条件为t不为0,而第三条orl指令的执行条件为t不为0且t+16不为0。这几个行为是在编译期完成的,也就是说在本例中,dynamic_cast由编译器在编译期实现了转换,所以可以说其是静态转换。
在前面的内存布局中,derived对象有3个偏移量,分别为(derived/base1 = 0, base2 = +0x10),即相对于derived和base1其偏移量为0,而相对于base2其偏移量为16。前两个dynamic_cast是derived* -> derived* 和 derived* -> base1*,都不需要调整指针,所以在checktype的if语句中使用t的值作为dynamic_cast的返回值。在第三次derived* -> base2*转换中,编译时知道地址是t+0x10,所以计算t+0x10的结果就是dynamic_cast的返回值。
至此,我们可以说,dynamic_cast操作中,向上转换是静态操作,在编译阶段完成。
向下转换 在checktype(base1*)处,通过gdb进行分析,如下:
(gdb) disasdump of assembler code for function _z9checktypeip5base1eit_:   0x0000000000400a10 : push   %rbp   0x0000000000400a11 : mov    %rsp,%rbp   0x0000000000400a14 : sub    $0x20,%rsp   0x0000000000400a18 : mov    %rdi,-0x18(%rbp)=> 0x0000000000400a1c : movl   $0x0,-0x4(%rbp)   0x0000000000400a23 : mov    -0x18(%rbp),%rax   0x0000000000400a27 : test   %rax,%rax   0x0000000000400a2a : je     0x400a4f    0x0000000000400a2c : mov    $0x0,%ecx ; src2dst = 0   0x0000000000400a31 : mov    $0x400c98,%edx ; dst_type   0x0000000000400a36 : mov    $0x400cf8,%esi ; src_type   0x0000000000400a3b : mov    %rax,%rdi   0x0000000000400a3e : callq  0x4006d0    0x0000000000400a43 : test   %rax,%rax   0x0000000000400a46 : je     0x400a4f    0x0000000000400a48 : mov    $0x1,%eax   0x0000000000400a4d : jmp    0x400a54    0x0000000000400a4f : mov    $0x0,%eax   0x0000000000400a54 : test   %al,%al   0x0000000000400a56 : je     0x400a5c    0x0000000000400a58 : orl    $0x1,-0x4(%rbp)      0x0000000000400a5c : cmpq   $0x0,-0x18(%rbp)   0x0000000000400a61 : je     0x400a67    0x0000000000400a63 : orl    $0x2,-0x4(%rbp)      0x0000000000400a67 : mov    -0x18(%rbp),%rax   0x0000000000400a6b : test   %rax,%rax   0x0000000000400a6e : je     0x400a95    0x0000000000400a70 : mov    $0xfffffffffffffffe,%rcx ; src2dst = -2   0x0000000000400a77 : mov    $0x400ce0,%edx ; dst_type   0x0000000000400a7c : mov    $0x400cf8,%esi ; src_type   0x0000000000400a81 : mov    %rax,%rdi   0x0000000000400a84 : callq  0x4006d0    0x0000000000400a89 : test   %rax,%rax   0x0000000000400a8c : je     0x400a95    0x0000000000400a8e : mov    $0x1,%eax   0x0000000000400a93 : jmp    0x400a9a       0x0000000000400a95 : mov    $0x0,%eax   0x0000000000400a9a : test   %al,%al   0x0000000000400a9c : je     0x400aa2    0x0000000000400a9e : orl    $0x4,-0x4(%rbp)      0x0000000000400aa2 : mov    -0x4(%rbp),%eax   0x0000000000400aa5 : leaveq---type  to continue, or q  to quit---   0x0000000000400aa6 : retqend of assembler dump. 通过上述汇编代码,很明显可以看出,base1* -> base1*不进行任何转换(这不废话嘛,类型是相同的)。而对于base1* -> derived* 以及 base1* -> base2* 则需要调用__dynamic_cast函数,而其所需要的参数,在汇编指令中也可以看出,下面将对该函数进行详细分析。
__dynamic_cast参数语义 声明如下:
__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 在上述声明中:
• src_ptr代表需要转换的指针
• src_type原始类型
• dst_type目标类型
• src2dst表示从dst到src的偏移量,当该值为如下3个之一时候,有特殊含义:
• -1: no hint
• -2: src is not a public base of dst
• -3: src is a multiple public base type but never a virtual base type
src2dst的值中,-2代表src 不是 dst 的公共基类,如上节中的base1* -> base2*;-3代表src是多个(dst的)公共基类并且不是虚基类,即没有虚拟继承的菱形继承。如果不为-1 -2 -3三值之一,则src2dst代表src和dst的偏移,如上一节中从base1* -> base1*转换的时候传值为0,即偏移为0;base1*->base2*转换的时候,传的值为-2(0xfffffffffffffffe)。
__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  {  const void *vtable = *static_cast  (src_ptr);  const vtable_prefix *prefix =      adjust_pointer  (vtable,              -offsetof (vtable_prefix, origin));  const void *whole_ptr =      adjust_pointer  (src_ptr, prefix->whole_object);  const __class_type_info *whole_type = prefix->whole_type;  __class_type_info::__dyncast_result result;  // if the whole object vptr doesn't refer to the whole object type, we're  // in the middle of constructing a primary base, and src is a separate  // base.  this has undefined behavior and we can't find anything outside  // of the base we're actually constructing, so fail now rather than  // segfault later trying to use a vbase offset that doesn't exist.  const void *whole_vtable = *static_cast  (whole_ptr);  const vtable_prefix *whole_prefix =    adjust_pointer  (whole_vtable,            -offsetof (vtable_prefix, origin));  const void *whole_vtable = *static_cast  (whole_ptr);  const vtable_prefix *whole_prefix =    (adjust_pointer      (whole_vtable, -ptrdiff_t (offsetof (vtable_prefix, origin))));  if (whole_prefix->whole_type != whole_type)    return null;  // avoid virtual function call in the simple success case.  if (src2dst >= 0      && src2dst == -prefix->whole_object      && *whole_type == *dst_type)    return const_cast  (whole_ptr);  whole_type->__do_dyncast (src2dst, __class_type_info::__contained_public,                            dst_type, whole_ptr, src_type, src_ptr, result);... 这个函数先通过src_ptr来初始化部分局部变量:
• vtable 通过对src_ptr解引用(deref)获取
• vtable_prefix 子对象虚函数表地址,通过vtable的类型信息和offset_to_top来获取
• whole_ptr src_ptr最底层的派生类地址,一般为src_ptr的值加上offset_to_top
• whole_type src_ptr最底层的派生类的虚函数表中的类型信息(type info)
• whole_vtable whole对象的虚函数表地址
然后调用whole_type->__do_dyncast,而这也是该函数的核心模块。然后根据返回值的内容来判断结果,并进行相应的操作。
其中,vtable_prefix的定义如下:
struct vtable_prefix {  // offset to most derived object.  ptrdiff_t whole_object;  // pointer to most derived type_info.  const __class_type_info *whole_type;   // what a class's vptr points to.  const void *origin;               }; • whole_object 表示当前指针指向对象的偏移量
• whole_type 指向 c++ 对象的类型:class(基类)、si_class(单一继承类型)、vmi_class(多重或虚拟继承类型)
• origin 表示虚函数表的入口,等于实例的虚指针。origin在这里的作用是offsetof,反向获取whole_object的指针。
__class_type_info::__dyncast_result 定义如下:
struct __class_type_info::__dyncast_result{  const void *dst_ptr;        // pointer to target object or null  __sub_kind whole2dst;       // path from most derived object to target  __sub_kind whole2src;       // path from most derived object to sub object  __sub_kind dst2src;         // path from target to sub object  int whole_details;          // details of the whole class hierarchy... 在前面提到,__do_dyncast被调用之后,后面就根据其出参result的返回值进行各种判断,那么result到底什么意思呢?其实,从上述定义就能看出,whole2dst代表whole对象向dst的转换结果,而whole2src代表whole对象向src的转换结果等,通过下面的图能更加清晰的理解转换过程:
在上图中,有3中类型,src、whole以及dst,__do_dyncast函数功能则是提供该3中类型的转换结果,在只有满足以下3中情况时候,__dynamic_cast才返回非空:
• src是dst的公共基类
• dst和src不是直接继承的关系,但是whole2src和whole2dst都是public
• dst2src未知且whole2src是非public虚继承关系,则不使用whole,重新获取dst和src的关系
这块逻辑比较绕,其实可以将关系理解为图上的一条条连接线,节点理解为类型信息,dynamic_cast的过程,就是判断有没有从src到dst有没有路径的过程。
继承关系 在前面的内容中,遇到过vtable for __cxxabiv1::__si_class_type_info+16这种,那么si_class_type_info又是什么呢?同样,在翻阅了源码之后,发现其是gcc中继承关系的一种。
在gcc中,将继承关系表示为图结构,对于类,有以下三种类型(type info):
• class __class_type_info : public std::type_info
• class __si_class_type_info : public __class_type_info
• class __vmi_class_type_info : public __class_type_info
其中,__class_type_info 表示没有继承关系的类,__si_class_type_info 表示单继承的类,__vmi_class_type_info 表示多继承或虚拟继承的类。类名开头的si代表单继承,vmi代表虚拟或多重继承。
查看定义,__si_class_type_info 包含指向基类类型的单个指针,而 __vmi_class_type_info 包含指向基类类型的指针数组。基类类型存储其子对象的位置和基类的类型(public、virtual)。
仍然以上一节中的代码为例,使用gdb来分析__zti7derived、__zti5base1、__zti5base2的关系
(gdb) x/2xg &_zti7derived0x555555755d80 :        0x00007ffff7dca5d8      0x0000555555554d74(gdb) x/2xg 0x00007ffff7dca5d80x7ffff7dca5d8 :  0x00007ffff7ae0920      0x00007ffff7ae0940(gdb) p *(__cxxabiv1::__vmi_class_type_info*)0x555555755d80$2 = {   = {     = {      _vptr.type_info = 0x7ffff7dca5d8 ,      __name = 0x555555554d74  7derived    }, },  members of __cxxabiv1:  __flags = 0,  __base_count = 2,  __base_info = {{      __base_type = 0x555555755dc8 ,      __offset_flags = 2    }}(gdb) p (*(__cxxabiv1::__vmi_class_type_info*)0x555555755d80)->__base_info[0]$4 = {  __base_type = 0x555555755dc8 ,  __offset_flags = 2     __base_info[1]$5 = {  __base_type = 0x555555755db8 ,  __offset_flags = 4098  <---- __public_mask(2) | offset:0x10}(gdb) x/2xg 0x555555755dc80x555555755dc8 :       0x00007ffff7dc98d8      0x0000555555554d7b(gdb) x/2xg 0x00007ffff7dc98d80x7ffff7dc98d8 :      0x00007ffff7add930      0x00007ffff7add950(gdb) x/2xg 0x555555755db80x555555755db8 :       0x00007ffff7dc98d8      0x0000555555554d77(gdb) x/2xg 0x00007ffff7dc98d80x7ffff7dc98d8 :      0x00007ffff7add930      0x00007ffff7add950(gdb) p *(__cxxabiv1::__class_type_info*)0x555555755dc8$6 = {   = {    _vptr.type_info = 0x7ffff7dc98d8 ,    __name = 0x555555554d7b  5base1  }, }(gdb) p *(__cxxabiv1::__class_type_info*)0x555555755db8$7 = {   = {    _vptr.type_info = 0x7ffff7dc98d8 ,    __name = 0x555555554d77  5base2  }, } 通过上述代码,可以看出_zti7derived是__vmi_class_type_info的一个实例,其基类数组的类型分别是_zti5base1和_zti5base2,通过将这些类型展开,就能获取一张图结构,进而说明dynamic_cast的过程就是遍历图结构确定路径关系的过程,采用的是深度优先搜索。


为什么MOSFET栅极与源极之间要加一个电阻?
什么是ARPANET 有何特点
述一加5受热捧程度,国内开体验会,国外排长龙
GPS真的只能用来定位导航吗
中芯国际调高去年Q4毛利率至30%,保持20%CAGR目标
C++:从技术实现角度聊聊RTTI
自给自足的电气化生态系统,将帮助我们更好地保护我们的星球
Allegro推出带有先进诊断功能和集成式电容器的全新可编程线性霍尔效应IC
中芯国际面临的四大挑战
人工智能正在掀起一场不可逆的数字化转型浪潮
采用国产统一操作系统UOS的龙芯电脑可以出售了
汽车氧传感器工作原理及其失效原因
下一步在促进新能源汽车产业发展方面会有什么新举措
智擎信息借助微软平台帮助模型训练与模拟测试节省大量时间
爱芯元智入选投中2023年度“锐公司100榜单”
首条8.5代TFT-LCD玻璃基板生产线
什么是无线网桥?全方位解析无线网桥及应用场景
人工智能和人员技能组合将成船舶行业趋势
mig接口的读写时序
2022年度关键词,回顾LED显示屏行业发展这一年