C++ RTTI(运行时类型识别)

432 阅读1分钟

Code

#include <iostream>
#include <typeinfo>

using namespace std;

class Base {
public:
    Base() {}
    ~Base() {}
    virtual void Print() {
        cout << "Base" << endl;
    }
    int b;
};

class Derive : public Base {
public:
    virtual void Print() {
        cout << "Derive" << endl;
    }
    int d;
};

int main(int argc, char** argv) {
    Base* bb = new Derive();
    cout << "Output0: " << typeid(bb).name() << endl;
    cout << "Output1: " << typeid(*bb).name() << endl;
    cout << "Output2: " << dynamic_cast<Derive*>(bb) << endl;
    cout << "Output3: " << dynamic_cast<Derive*>(new Base()) << endl;
    return 0;
}

Output

xiaoju@XiaojuVM ~/self_src/demo/cpp $ g++ -g  hello.cpp
xiaoju@XiaojuVM ~/self_src/demo/cpp $ ./a.out
Output0: P4Base
Output1: 6Derive
Output2: 0x13e8040
Output3: 0

RTTI 的两种方式

typeid

typeid是c++进行类型识别的运算符,返回一个type_info用于描述对应参数的类型信息,type_info核心定义如下

class type_info
{
public:
    virtual ~type_info();
    
    /** Returns an @e implementation-defined byte string; this is not
     *  portable between compilers!  */
    const char* name() const ;

    /** Returns true if @c *this precedes @c __arg in the implementation's
     *  collation order.  */
    bool before(const type_info& __arg) ;

    bool operator==(const type_info& __arg) const;
    bool operator!=(const type_info& __arg) const ;
 protected:
    const char *__name;
  };
  • 编译期类型识别 类似于Output0,编译期编译器就知道bb的类型是指向Base的指针了,编译期就知道typeid的返回是什么
  • 运行期类型识别 类似于Output1,由于Derive继承Base实现了多态,编译期无法确定*bb的类型是Derive还是Base,只有等到运行时才知道,才可以确定typeid的返回

dynamic_cast

dynamic_cast是c++进行类型转换的运算符,可以安全的进行父类指针的向下类型转换
若父类指针无法转型为对应子类,则返回一个NULL指针,类似于Output3,否则,返回对应子类类型指针,类似于Output2

底层实现

内存布局

image.png

内存布局分析

首先通过gdb看一下vtbl位置

(gdb) l
21	    int d;
22	};
23
24	int main(int argc, char** argv) {
25	    Base* bb = new Derive();
26	    cout << "Output0: " << typeid(bb).name() << endl;
27	    cout << "Output1: " << typeid(*bb).name() << endl;
28	    cout << "Output2: " << dynamic_cast<Derive*>(bb) << endl;
29	    cout << "Output3: " << dynamic_cast<Derive*>(new Base()) << endl;
30	    return 0;
(gdb) p *bb
$1 = {_vptr.Base = 0x400e60 <vtable for Derive+16>, b = 0}
(gdb) p bb
$2 = (Base *) 0x603040
(gdb)

可以看到,bb的值0x603040,vptr是0x400e60,由于vtbl位于.rodata只读数据段,下面通过objdump看一下.rodata只读数据段内容

image.png 可以看到vptr[0]位置(0x400e60处)代表第一个虚函数的函数地址,具体的函数地址应该是0x400d1e(字节序),通过objdump反汇编可以看到具体二进制代码

xiaoju@XiaojuVM ~/self_src/demo/cpp $ objdump -S -j .text a.out

0000000000400d1e <_ZN6Derive5PrintEv>:

class Derive : public Base {
public:
    virtual void Print() {
  400d1e:	55                   	push   %rbp
  400d1f:	48 89 e5             	mov    %rsp,%rbp
  400d22:	48 83 ec 10          	sub    $0x10,%rsp
  400d26:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)
        cout << "Derive" << endl;
  400d2a:	be 15 0e 40 00       	mov    $0x400e15,%esi
  400d2f:	bf e0 20 60 00       	mov    $0x6020e0,%edi
  400d34:	e8 47 fc ff ff       	callq  400980 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
  400d39:	be d0 09 40 00       	mov    $0x4009d0,%esi
  400d3e:	48 89 c7             	mov    %rax,%rdi
  400d41:	e8 7a fc ff ff       	callq  4009c0 <_ZNSolsEPFRSoS_E@plt>
    }
  400d46:	c9                   	leaveq
  400d47:	c3                   	retq

同时可以看到,vptr[-1]位置(0x400e58处)代表bb实际指向数据类型的type_info的地址,为0x400ed0,因此可以证明,类的type_info信息,与vtbl一样,是保存在.rodata只读数据段的

image.png 在0x400ed0处,第一个word是0x602210,代表type_info的vptr,第二个word 0x400ec0,代表 __name 成员变量(char*类型)的值,可见,__name所指向的字符串为6Derive,为Derive类型的类型名。

实现原理

  • typeid 运行时类型识别 编译器将
typeid(*bb)

转化为

*((type_info*)bb->vptr[-1])

若bb实际指向类型为Base,则bb->vptr指向Base的vtbl,bb->vptr[-1]指向Base的type_info
若bb实际指向类型为Derive,则bb->vptr指向Derive的vtbl,bb->vptr[-1]指向Derive的type_info
依此实现运行时进行类型识别

  • dynamic_cast 编译器将
dynamic_cast<Derive*>(bb)

转化为

Derive* dynamicCast(Base* bb) {
    const type_info& bb_type = *((type_info*)bb->vptr[-1]); // 获取bb实际指向数据类型信息
    if (bb_type == typeid(Derive)) {
        return (Derive*)bb;
    }
    return NULL;
}

核心实现逻辑大致如此,内部细节定有不同