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

发表于 1年以前  | 总阅读数:309 次

第一次接触RTTI,是在<<深度探索c++对象模型>>这本书中,当时对这块的理解比较浅,可能因为知识积累不足吧。后面在工作中用到的越来越多,也逐渐加深了对其认识,但一直没有一个系统的认知,所以抽出一段时间,把这块内容整理下。

背景

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 <cassert>
#include <iostream>
#include <typeinfo>

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的标准库类型的对象的引用(在头文件<typeinfo>中定义),或者说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 <cxxabi.h>
#include <iostream>
#include <memory>
#include <string>
#include <typeinfo>
#include <vector>

std::string demangle(const char* name) {
  int status = -4;
  std::unique_ptr<char, void(*)(void*)> res {
         abi::__cxa_demangle(name, NULL, NULL, &status),
                 std::free
         };
  return (status==0) ? res.get() : name ;
}

int main() {
  std::vector<int> v;
  std::cout << "before: " << typeid(v).name() << " after: " << demangle(typeid(v).name()) << std::endl;
  return 0;
}

输出如下:

before: St6vectorIiSaIiEE after: std::vector<int, std::allocator<int> >

下面是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 <iostream>
#include <typeinfo>

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
        ret
vtable 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 "P4Base"
typeinfo for Base*:
        .quad   vtable for __cxxabiv1::__pointer_type_info+16
        .quad   typeinfo name for Base*
        .long   0
        .zero   4
        .quad   typeinfo for Base
typeinfo name for Derived:
        .string "7Derived"
typeinfo for Derived:
        .quad   vtable for __cxxabiv1::__si_class_type_info+16
        .quad   typeinfo name for Derived
        .quad   typeinfo for Base
typeinfo name for Base:
        .string "4Base"
typeinfo 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 Base
Base::_ZTV4Base: 3u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI4Base)
16    (int (*)(...))Base::fun

Class Base
   size=8 align=8
   base size=8 base align=8
Base (0x0x7f59773402a0) 0 nearly-empty
    vptr=((& Base::_ZTV4Base) + 16u)

Vtable for Derived
Derived::_ZTV7Derived: 3u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI7Derived)
16    (int (*)(...))Derived::fun

Class Derived
   size=8 align=8
   base size=8 base align=8
Derived (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 _ZTI7Derived
typeinfo for Derived

非多态

代码如下:


#include <iostream>
#include <string>
#include <typeinfo>

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
        ret
typeinfo name for MyClss:
        .string "6MyClss"
typeinfo 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 <iostream>
#include <typeinfo>

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<Base2*>(d); // upcasting 向上转换
  Derived *d1 = dynamic_cast<Derived*>(b1); // downcasting 向下转换

  return 0;
}

实现

通过查阅资料,发现dynamic_cast最终会调用libstdc++中的__dynamic_cast函数,所以曾经以为__dynamic_cast函数就是dynamic_cast的实现版本,但是通过对比参数,发现并非如此:

dynamic_cast<T*>(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 <iostream>
#include <typeinfo>

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 <class T>
int CheckType(T t) {
  int n = 0;
  if (dynamic_cast<Derived*>(t)) {
    n |= 1;
  } 
  if (dynamic_cast<Base1*>(t)) {
    n |= 2;
  }
  if (dynamic_cast<Base2*>(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 (*)(...))-16Base2 (0x0x7f7fbbe5b6c0) 16部分):

Vtable for Derived
Derived::_ZTV7Derived: 7u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI7Derived)
16    (int (*)(...))Base1::f1
24    (int (*)(...))Derived::f2
32    (int (*)(...))-16
40    (int (*)(...))(& _ZTI7Derived)
48    (int (*)(...))Derived::_ZThn16_N7Derived2f2Ev

Class Derived
   size=32 align=8
   base size=32 base align=8
Derived (0x0x7f7fbbf10c40) 0
    vptr=((& Derived::_ZTV7Derived) + 16u)
  Base1 (0x0x7f7fbbe5b660) 0
      primary-for Derived (0x0x7f7fbbf10c40)
  Base2 (0x0x7f7fbbe5b6c0) 16
      vptr=((& Derived::_ZTV7Derived) + 48u)

向上转换

在CheckType(Derived*)处,通过gdb进行分析,如下:

(gdb) disas
Dump of assembler code for function _Z9CheckTypeIP7DerivedEiT_:
   0x00000000004009ce <+0>: push   %rbp
   0x00000000004009cf <+1>: mov    %rsp,%rbp
   0x00000000004009d2 <+4>: mov    %rdi,-0x18(%rbp)
=> 0x00000000004009d6 <+8>: movl   $0x0,-0x4(%rbp)

   0x00000000004009dd <+15>: cmpq   $0x0,-0x18(%rbp)
   0x00000000004009e2 <+20>: je     0x4009e8 <_Z9CheckTypeIP7DerivedEiT_+26>
   0x00000000004009e4 <+22>: orl    $0x1,-0x4(%rbp) ; if t != nullptr

   0x00000000004009e8 <+26>: cmpq   $0x0,-0x18(%rbp)
   0x00000000004009ed <+31>: je     0x4009f3 <_Z9CheckTypeIP7DerivedEiT_+37>
   0x00000000004009ef <+33>: orl    $0x2,-0x4(%rbp) ; if t != nullptr

   0x00000000004009f3 <+37>: cmpq   $0x0,-0x18(%rbp)
   0x00000000004009f8 <+42>: je     0x400a0b <_Z9CheckTypeIP7DerivedEiT_+61>
   0x00000000004009fa <+44>: mov    -0x18(%rbp),%rax
   0x00000000004009fe <+48>: add    $0x10,%rax
   0x0000000000400a02 <+52>: test   %rax,%rax
   0x0000000000400a05 <+55>: je     0x400a0b <_Z9CheckTypeIP7DerivedEiT_+61>
   0x0000000000400a07 <+57>: orl    $0x4,-0x4(%rbp) ; if t != nullptr && t + 0x10 != nullptr
   0x0000000000400a0b <+61>: mov    -0x4(%rbp),%eax
   0x0000000000400a0e <+64>: pop    %rbp
   0x0000000000400a0f <+65>: retq
End 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) disas
Dump of assembler code for function _Z9CheckTypeIP5Base1EiT_:
   0x0000000000400a10 <+0>: push   %rbp
   0x0000000000400a11 <+1>: mov    %rsp,%rbp
   0x0000000000400a14 <+4>: sub    $0x20,%rsp
   0x0000000000400a18 <+8>: mov    %rdi,-0x18(%rbp)
=> 0x0000000000400a1c <+12>: movl   $0x0,-0x4(%rbp)

   0x0000000000400a23 <+19>: mov    -0x18(%rbp),%rax
   0x0000000000400a27 <+23>: test   %rax,%rax
   0x0000000000400a2a <+26>: je     0x400a4f <_Z9CheckTypeIP5Base1EiT_+63>
   0x0000000000400a2c <+28>: mov    $0x0,%ecx ; src2dst = 0
   0x0000000000400a31 <+33>: mov    $0x400c98,%edx ; dst_type<_ZTV7Derived>
   0x0000000000400a36 <+38>: mov    $0x400cf8,%esi ; src_type<_ZTI5Base1>
   0x0000000000400a3b <+43>: mov    %rax,%rdi
   0x0000000000400a3e <+46>: callq  0x4006d0 <__dynamic_cast@plt>
   0x0000000000400a43 <+51>: test   %rax,%rax
   0x0000000000400a46 <+54>: je     0x400a4f <_Z9CheckTypeIP5Base1EiT_+63>
   0x0000000000400a48 <+56>: mov    $0x1,%eax
   0x0000000000400a4d <+61>: jmp    0x400a54 <_Z9CheckTypeIP5Base1EiT_+68>
   0x0000000000400a4f <+63>: mov    $0x0,%eax
   0x0000000000400a54 <+68>: test   %al,%al
   0x0000000000400a56 <+70>: je     0x400a5c <_Z9CheckTypeIP5Base1EiT_+76>
   0x0000000000400a58 <+72>: orl    $0x1,-0x4(%rbp)

   0x0000000000400a5c <+76>: cmpq   $0x0,-0x18(%rbp)
   0x0000000000400a61 <+81>: je     0x400a67 <_Z9CheckTypeIP5Base1EiT_+87>
   0x0000000000400a63 <+83>: orl    $0x2,-0x4(%rbp)

   0x0000000000400a67 <+87>: mov    -0x18(%rbp),%rax
   0x0000000000400a6b <+91>: test   %rax,%rax
   0x0000000000400a6e <+94>: je     0x400a95 <_Z9CheckTypeIP5Base1EiT_+133>
   0x0000000000400a70 <+96>: mov    $0xfffffffffffffffe,%rcx ; src2dst = -2
   0x0000000000400a77 <+103>: mov    $0x400ce0,%edx ; dst_type<_ZTI5Base2>
   0x0000000000400a7c <+108>: mov    $0x400cf8,%esi ; src_type<_ZTI5Base1>
   0x0000000000400a81 <+113>: mov    %rax,%rdi
   0x0000000000400a84 <+116>: callq  0x4006d0 <__dynamic_cast@plt>
   0x0000000000400a89 <+121>: test   %rax,%rax
   0x0000000000400a8c <+124>: je     0x400a95 <_Z9CheckTypeIP5Base1EiT_+133>
   0x0000000000400a8e <+126>: mov    $0x1,%eax
   0x0000000000400a93 <+131>: jmp    0x400a9a <_Z9CheckTypeIP5Base1EiT_+138>

   0x0000000000400a95 <+133>: mov    $0x0,%eax
   0x0000000000400a9a <+138>: test   %al,%al
   0x0000000000400a9c <+140>: je     0x400aa2 <_Z9CheckTypeIP5Base1EiT_+146>
   0x0000000000400a9e <+142>: orl    $0x4,-0x4(%rbp)

   0x0000000000400aa2 <+146>: mov    -0x4(%rbp),%eax
   0x0000000000400aa5 <+149>: leaveq
---Type <return> to continue, or q <return> to quit---
   0x0000000000400aa6 <+150>: retq
End 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 <const void *const *> (src_ptr);
  const vtable_prefix *prefix =
      adjust_pointer <vtable_prefix> (vtable,
              -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;

  // 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 <const void *const *> (whole_ptr);
  const vtable_prefix *whole_prefix =
    adjust_pointer <vtable_prefix> (whole_vtable,
            -offsetof (vtable_prefix, origin));
  const void *whole_vtable = *static_cast <const void *const *> (whole_ptr);
  const vtable_prefix *whole_prefix =
    (adjust_pointer <vtable_prefix>
     (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 <void *> (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 &_ZTI7Derived
0x555555755d80 <_ZTI7Derived>:        0x00007ffff7dca5d8      0x0000555555554d74
(gdb) x/2xg 0x00007ffff7dca5d8
0x7ffff7dca5d8 <_ZTVN10__cxxabiv121__vmi_class_type_infoE+16>:  0x00007ffff7ae0920      0x00007ffff7ae0940

(gdb) p *(__cxxabiv1::__vmi_class_type_info*)0x555555755d80
$2 = {
  <__cxxabiv1::__class_type_info> = {
    <std::type_info> = {
      _vptr.type_info = 0x7ffff7dca5d8 <vtable for __cxxabiv1::__vmi_class_type_info+16>,
      __name = 0x555555554d74 <typeinfo name for Derived> "7Derived"
    }, <No data fields>},
  members of __cxxabiv1::__vmi_class_type_info:
  __flags = 0,
  __base_count = 2,
  __base_info = {{
      __base_type = 0x555555755dc8 <typeinfo for Base1>,
      __offset_flags = 2
    }}

(gdb) p (*(__cxxabiv1::__vmi_class_type_info*)0x555555755d80)->__base_info[0]
$4 = {
  __base_type = 0x555555755dc8 <typeinfo for Base1>,
  __offset_flags = 2     <---- __public_mask(2) | offset:0x00
}
(gdb) p (*(__cxxabiv1::__vmi_class_type_info*)0x555555755d80)->__base_info[1]
$5 = {
  __base_type = 0x555555755db8 <typeinfo for Base2>,
  __offset_flags = 4098  <---- __public_mask(2) | offset:0x10
}

(gdb) x/2xg 0x555555755dc8
0x555555755dc8 <_ZTI5Base1>:       0x00007ffff7dc98d8      0x0000555555554d7b
(gdb) x/2xg 0x00007ffff7dc98d8
0x7ffff7dc98d8 <_ZTVN10__cxxabiv117__class_type_infoE+16>:      0x00007ffff7add930      0x00007ffff7add950

(gdb) x/2xg 0x555555755db8
0x555555755db8 <_ZTI5Base2>:       0x00007ffff7dc98d8      0x0000555555554d77
(gdb) x/2xg 0x00007ffff7dc98d8
0x7ffff7dc98d8 <_ZTVN10__cxxabiv117__class_type_infoE+16>:      0x00007ffff7add930      0x00007ffff7add950

(gdb) p *(__cxxabiv1::__class_type_info*)0x555555755dc8
$6 = {
  <std::type_info> = {
    _vptr.type_info = 0x7ffff7dc98d8 <vtable for __cxxabiv1::__class_type_info+16>,
    __name = 0x555555554d7b <typeinfo name for Base1> "5Base1"
  }, <No data fields>}

(gdb) p *(__cxxabiv1::__class_type_info*)0x555555755db8
$7 = {
  <std::type_info> = {
    _vptr.type_info = 0x7ffff7dc98d8 <vtable for __cxxabiv1::__class_type_info+16>,
    __name = 0x555555554d77 <typeinfo name for Base2> "5Base2"
  }, <No data fields>}

通过上述代码,可以看出_ZTI7Derived是__vmi_class_type_info的一个实例,其基类数组的类型分别是_ZTI5Base1和_ZTI5Base2,通过将这些类型展开,就能获取一张图结构,进而说明dynamic_cast的过程就是遍历图结构确定路径关系的过程,采用的是深度优先搜索

结语

断断续续写了一个月,期间经历了病毒的折磨,总算是完成了。

至此,算是对gcc的RTTI实现有了一个初步的认识,由于文章内容通过调查资料以及分析源码,所以可能会有理解偏差,欢迎指正。

好了,今天的文章就到这,我们下期见!

本文由哈喽比特于1年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/h_pgrd7Ui0zZwwTBJb0Sxg

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:7月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:7月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:7月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:7月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:7月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:7月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:7月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:7月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:7月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:7月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:7月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:7月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:7月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:7月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:7月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:7月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:7月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:7月以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  236860次阅读
vscode超好用的代码书签插件Bookmarks 1年以前  |  6844次阅读
 目录