C++ RTTI详解

     阅读:47

1、RTTI简介

RTTI(Runtime Type Identification)是“运行时类型识别”的意思。C++引入这个机制是为了让程序在运行时能根据基类的指针或引用来获得该指针或引用所指的对象的实际类型。但是现在RTTI的类型识别已经不限于此了,它还能通过typeid操作符识别出所有的基本类型的变量对应的类型。为什么会出现RTTI这一机制呢?这和C++语言本身有关系,C++是一门静态类型语言,其数据类型是在编译期就确定的,不能在运行时更改。然而由于面向对象程序设计中多态性的要求,C++中的指针或引用本身的类型,可能与它实际代表的类型并不一致,有时我们需要将一个多态指针转换为其实际指向对象的类型,就需要知道运行时的类型信息,这就有了运行时类型识别需求。和Java相比,C++要想获得运行时类型信息,只能通过RTTI机制,并且C++最终生成的代码是直接与机器相关的。

相关资料:Java中任何一个类都可以通过反射机制来获取类的基本信息(接口、父类、方法、属性、Annotation等),而且Java中还提供了一个关键字,可以在运行时判断一个类是不是另一个类的子类或者是该类的对象,Java可以生成字节码文件,再由JVM(Java虚拟机)加载运行,字节码文件中可以含有类的信息。

C++通过以下两个关键字提供RTTI功能:

  1. typeid:该运算符返回其表达式或类型名的实际类型
  2. dynamic_cast:该运算符将基类的指针或引用安全地转换为派生类类型的指针或引用(也就是所谓的下行转换)

本文将重点介绍这两个关键字,在阅读后续内容之前建议读者先看一下以下三篇文章,对理解本文会有巨大帮助:

2、typeid详解

2.1、type_info类介绍

typeid的返回值是const type_info&类型的数据,下面是type_infogcc-4.9.0中的定义(位于libstdc++-v3\libsupc++\typeinfo文件中)

class type_info
{
public:
    virtual ~type_info();

    const char* name() const _GLIBCXX_NOEXCEPT
    { return __name[0] == '*' ? __name + 1 : __name; }

#if !__GXX_TYPEINFO_EQUALITY_INLINE
    bool before(const type_info& __arg) const _GLIBCXX_NOEXCEPT;
    bool operator==(const type_info& __arg) const _GLIBCXX_NOEXCEPT;
#else
    #if !__GXX_MERGED_TYPEINFO_NAMES
        bool before(const type_info& __arg) const _GLIBCXX_NOEXCEPT
        { return (__name[0] == '*' && __arg.__name[0] == '*') ?
              __name < __arg.__name : __builtin_strcmp (__name, __arg.__name) < 0;
        }

        bool operator==(const type_info& __arg) const _GLIBCXX_NOEXCEPT
        {
            return ((__name == __arg.__name) ||
                (__name[0] != '*' && __builtin_strcmp (__name, __arg.__name) == 0));
        }
    #else
        bool before(const type_info& __arg) const _GLIBCXX_NOEXCEPT
        { return __name < __arg.__name; }

        bool operator==(const type_info& __arg) const _GLIBCXX_NOEXCEPT
        { return __name == __arg.__name; }
    #endif
#endif

    bool operator!=(const type_info& __arg) const _GLIBCXX_NOEXCEPT
    { return !operator==(__arg); }

#if __cplusplus >= 201103L
    size_t hash_code() const noexcept
    {
    #  if !__GXX_MERGED_TYPEINFO_NAMES
        return _Hash_bytes(name(), __builtin_strlen(name()),
             static_cast<size_t>(0xc70f6907UL));
    #  else
        return reinterpret_cast<size_t>(__name);
    #  endif
    }
#endif // C++11

    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&);
};

从源代码中可以看出以下几点内容:

  1. 有一个类成员__name,类型是const char*,这个指针最终会指向类型的名字
  2. 我们不能直接实例化类type_info的对象,因为该类的正常构造函数是保护的,要构造type_info对象的唯一方法就是使用typeid运算符。
  3. 由于重载的赋值运算符和拷贝构造函数也是私有的,因此我们不能自己去复制或分配类type_info的对象。
  4. 其余那些成员方法大家就自己看一下吧,都是一些从名字就能看出用法的函数,比如name()返回类型名,__is_pointer_p()返回是否是指针类型等等

2.2、typeid识别静态类型

typeid中的操作数是以下任意一种时,typeid得出的是静态类型,即编译时就确定的类型:

  • 一个任意的类型名
  • 一个基本内置类型的变量,或指向基本内置类型的指针或引用
  • 一个任意类型的指针(指针就是指针,本身不体现多态,多指针解引用才有可能会体现多态)
  • 一个具体的对象实例,无论对应的类有没有多态都可以直接在编译器确定
  • 一个指向没有多态的类对象的指针的解引用
  • 一个指向没有多态的类对象的引用

由于静态类型在程序的运行过程中并不会改变,所以并不需要等到程序运行时再去推算其类型,在编译时期就能根据操作数的静态类型,从而推导出其具体类型信息。我们先来看如下一段代码:

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

int main(int argc, char* argv[])
{
    std::cout << typeid(char).name() << std::endl;
    std::cout << typeid(int).name() << std::endl;
    std::cout << typeid(double).name() << std::endl;

    std::cout << "----------" << std::endl;

    std::cout << typeid(unsigned char).name() << std::endl;
    std::cout << typeid(unsigned int).name() << std::endl;

    std::cout << "----------" << std::endl;

    std::cout << typeid(std::string).name() << std::endl;
    std::cout << typeid(std::vector<float>).name() << std::endl;
}

运行结果如下(gcc-4.8.5):

c
i
d
----------
h
j
----------
Ss
St6vectorIfSaIfEE

看完我有点怀疑我的智商和人生,不是,是码生了,前面几个还好,还能大概看出是什么类型,但是后面那几个是什么鬼,尤其是这个St6vectorIfSaIfEE,谁能看出是std::vector<float>了?这些其实是被编译器转换过后的类型名,typeid也只能获取这种形式的名称。由于直接看这个实在有点反人类,所以我们只能动用点非常手段了,修改代码如下:

#include <iostream>
#include <string>
#include <vector>
#include <typeinfo>
#include <cxxabi.h>

const char* TypeToName(const char* name)
{
    const char* __name = abi::__cxa_demangle(name, nullptr, nullptr, nullptr);
    return __name;
}

int main(int argc, char* argv[])
{
    std::cout << TypeToName(typeid(char).name()) << std::endl;
    std::cout << TypeToName(typeid(int).name()) << std::endl;
    std::cout << TypeToName(typeid(double).name()) << std::endl;

    std::cout << "----------" << std::endl;

    std::cout << TypeToName(typeid(unsigned char).name()) << std::endl;
    std::cout << TypeToName(typeid(unsigned int).name()) << std::endl;

    std::cout << "----------" << std::endl;

    std::cout << TypeToName(typeid(std::string).name()) << std::endl;
    std::cout << TypeToName(typeid(std::vector<float>).name()) << std::endl;
}

运行结果如下,现在可以看到的内容就正常多了,这里其实就是用到了abi::__cxa_demangle这个方法获取类型的真实名称,关于这部分的内容这里就不细讲了,有兴趣的读者可以去看这篇文章《C++封装一个易用的打印backtrace信息的函数》,里面就有相应的介绍。

char
int
double
----------
unsigned char
unsigned int
----------
std::string
std::vector<float, std::allocator >

有了前面小小的铺垫,我们可以着手编写测试代码了:

#include <iostream>
#include <string>
#include <vector>
#include <typeinfo>
#include <cxxabi.h>

const char* TypeToName(const char* name)
{
    const char* __name = abi::__cxa_demangle(name, nullptr, nullptr, nullptr);
    return __name;
}

class A
{
public:
    void print()
    {
        std::cout << "A" << std::endl;
    }
};

class B : public A
{
public:
    void print()
    {
        std::cout << "B" << std::endl;
    }
};

class C
{
public:
    virtual void print()
    {
        std::cout << "C" << std::endl;
    }
};

class D : public C
{
public:
    virtual void print()
    {
        std::cout << "D" << std::endl;
    }
};

int main(int argc, char* argv[])
{
    std::cout << "---------- 一个任意的类型名 ----------" << std::endl;
    std::cout << "----- 基本内置类型名 -----" << std::endl;
    std::cout << TypeToName(typeid(char).name()) << std::endl;
    std::cout << TypeToName(typeid(int).name()) << std::endl;
    std::cout << "----- 无多态的类型名 -----" << std::endl;
    std::cout << TypeToName(typeid(std::string).name()) << std::endl;
    std::cout << TypeToName(typeid(std::vector<int>).name()) << std::endl;
    std::cout << "----- 有多态的类型名 -----" << std::endl;
    std::cout << TypeToName(typeid(std::iostream).name()) << std::endl;

    std::cout << "\n\n" << "---------- 一个基本内置类型的变量,或指向基本内置类型的指针或引用 ----------" << std::endl;
    std::cout << "----- 基本内置类型的变量 -----" << std::endl;
    long type_long = 1;
    double type_double = 1.23;
    std::cout << TypeToName(typeid(type_long).name()) << std::endl;
    std::cout << TypeToName(typeid(type_double).name()) << std::endl;
    std::cout << "----- 指向基本内置类型的指针或引用 -----" << std::endl;
    long* type_long_ptr = &type_long;
    double& type_double_ref = type_double;
    std::cout << TypeToName(typeid(type_long_ptr).name()) << std::endl;
    std::cout << TypeToName(typeid(type_double_ref).name()) << std::endl;

    std::cout << "\n\n" << "---------- 一个任意类型的指针 ----------" << std::endl;
    std::cout << "----- 一个指向没有多态类型的指针 -----" << std::endl;
    B b;
    A* a_ptr = &b;
    std::cout << TypeToName(typeid(&b).name()) << std::endl;
    std::cout << TypeToName(typeid(a_ptr).name()) << std::endl;
    std::cout << "----- 一个指向具有多态类型的指针 -----" << std::endl;
    D d;
    C* c_ptr = &d;
    std::cout << TypeToName(typeid(&d).name()) << std::endl;
    std::cout << TypeToName(typeid(c_ptr).name()) << std::endl;

    std::cout << "\n\n" << "---------- 一个具体的对象实例,无论对应的类有没有多态都可以直接在编译器确定 ----------" << std::endl;
    std::cout << "----- 无多态类的实例 -----" << std::endl;
    std::string type_string;
    std::vector<int> type_vector;
    std::cout << TypeToName(typeid(type_string).name()) << std::endl;
    std::cout << TypeToName(typeid(type_vector).name()) << std::endl;
    std::cout << "----- 有多态类的实例 -----" << std::endl;
    std::iostream type_ios(nullptr);
    std::cout << TypeToName(typeid(type_ios).name()) << std::endl;

    std::cout << "\n\n" << "---------- 一个指向没有多态的类对象的指针的解引用 ----------" << std::endl;
    std::cout << "----- 一个指向没有多态的类对象的指针的解引用 -----" << std::endl;
    std::cout << TypeToName(typeid(*a_ptr).name()) << std::endl; // 推算出来的依然是A

    std::cout << "\n\n" << "---------- 一个指向没有多态的类对象的引用 ----------" << std::endl;
    std::cout << "----- 一个指向没有多态的类对象的引用 -----" << std::endl;
    A& a_ref = b;
    std::cout << TypeToName(typeid(a_ref).name()) << std::endl; // 推算出来的依然是A
}

运行结果如下,前面讲的六种静态类型都涉及到了,大家看一下就能明白是怎么一回事了:

---------- 一个任意的类型名 ----------
----- 基本内置类型名 -----
char
int
----- 无多态的类型名 -----
std::string
std::vector<int, std::allocator >
----- 有多态的类型名 -----
std::iostream


---------- 一个基本内置类型的变量,或指向基本内置类型的指针或引用 ----------
----- 基本内置类型的变量 -----
long
double
----- 指向基本内置类型的指针或引用 -----
long*
double


---------- 一个任意类型的指针 ----------
----- 一个指向没有多态类型的指针 -----
B*
A*
----- 一个指向具有多态类型的指针 -----
D*
C*


---------- 一个具体的对象实例,无论对应的类有没有多态都可以直接在编译器确定 ----------
----- 无多态类的实例 -----
std::string
std::vector<int, std::allocator >
----- 有多态类的实例 -----
std::iostream


---------- 一个指向没有多态的类对象的指针的解引用 ----------
----- 一个指向没有多态的类对象的指针的解引用 -----
A


---------- 一个指向没有多态的类对象的引用 ----------
----- 一个指向没有多态的类对象的引用 -----
A

2.3、typeid识别动态类型

typeid中的操作数是以下任意一种时,typeid需要在程序运行时推算类型,因为其操作数的类型在编译时期是不能被确定的:

  • 一个指向含有多态的类对象的指针的解引用
  • 一个指向含有多态的类对象的引用

下面先来看一个典型错误案例,程序如下:

#include <iostream>
#include <string>
#include <vector>
#include <typeinfo>
#include <cxxabi.h>

const char* TypeToName(const char* name)
{
    const char* __name = abi::__cxa_demangle(name, nullptr, nullptr, nullptr);
    return __name;
}

class A
{
public:
    void print()
    {
        std::cout << "A" << std::endl;
    }

    int a;
};

class B : virtual public A
{
public:
    void print()
    {
        std::cout << "B" << std::endl;
    }

    int b;
};

class C : virtual public A
{
public:
    void print()
    {
        std::cout << "C" << std::endl;
    }

    int c;
};

class D : public B, public C
{
public:
    void print()
    {
        std::cout << "D" << std::endl;
    }

    int d;
};

int main(int argc, char* argv[])
{
    D d;
    A* a_ptr = &d;
    B* b_ptr = &d;
    C* c_ptr = &d;

    std::cout << TypeToName(typeid(d).name()) << std::endl;
    std::cout << TypeToName(typeid(*a_ptr).name()) << std::endl;
    std::cout << TypeToName(typeid(*b_ptr).name()) << std::endl;
    std::cout << TypeToName(typeid(*c_ptr).name()) << std::endl;
}

运行结果如下:

D
A
B
C

从运行结果可以看出上面的代码并没有体现出多态性,这里就有个疑问了,之前在这篇文章《一文读懂C++虚继承的内存模型》不是说虚继承也有虚表吗,怎么到这里体现不出多态性了?很遗憾,因为单纯虚继承是无法体现出多态的,尽管它也会生成对应的虚表,要真正实现多态只能依靠虚函数来实现。
修改类A如下,其余不变:

class A
{
public:
    virtual ~A() {}

    void print()
    {
        std::cout << "A" << std::endl;
    }

    int a;
};

运行结果如下:

D
D
D
D

可以看到给类A加了个虚析构函数之后,多态性就体现出来了,typeid也可以正确识别出a_ptrb_ptrc_ptr引用的真实类型了。

2.4、typeid实现原理

接下来我们就来探讨一下typeid的实现原理,我们之前从《一文读懂C++虚函数的内存模型》和《一文读懂C++虚继承的内存模型》这两篇文章中知道了虚函数表指针指向的前一个位置(也就是索引的-1项)存放的就是当前实际类型的type_info信息,当时还不知道这玩意到底有什么用,要怎么用,但现在就一目了然了,因为typeid的返回值类型就是type_info,所以我们可以很轻易写出如下程序(只修改main函数,其余代码与前面最后一个例子保持不变):

int main(int argc, char* argv[])
{
    D d;
    A* a_ptr = &d;
    B* b_ptr = &d;
    C* c_ptr = &d;

    std::cout << typeid(*a_ptr).name() << std::endl;
    long* vtbl_A = (long*)*(long*)a_ptr;
    std::cout << ((std::type_info*)*(vtbl_A - 1))->name() << std::endl << std::endl;

    std::cout << typeid(*b_ptr).name() << std::endl;
    long* vtbl_B = (long*)*(long*)b_ptr;
    std::cout << ((std::type_info*)*(vtbl_B - 1))->name() << std::endl << std::endl;

    std::cout << typeid(*c_ptr).name() << std::endl;
    long* vtbl_C = (long*)*(long*)c_ptr;
    std::cout << ((std::type_info*)*(vtbl_C - 1))->name() << std::endl;
}

运行结果如下:

1D
1D

1D
1D

1D
1D

从运行结果可以看到,我们在程序中将虚函数表的-1项的值转换成一个type_info的指针类型,并调用name()成员函数的最终的输出为1D,与typeid的输出一致,从而可以知道,typeid关于多态类型的计算是通过基类指针或引用指向的对象(子对象)的虚函数表的-1项获得的。

补充说明:在多继承或虚继承的情况下,派生类类有n(n>1)个虚函数指针,分别指向其各个基类的虚函数表的具体位置(事实上只有一个表,这些表是链接在一起的),但是一个类的所有虚函数表的索引为-1项的值(type_info对象的地址)都是相等的,即它们都指向同一个type_info对象,这样就实现了无论使用了哪一个基类的指针或引用指向其派生类的对象,都能通过相应的虚函数表获取到相同的type_info对象,从而得到相同的类型信息。

3、dynamic_cast详解

dynamic_cast获取运行时类型信息的原理和前面typeid是一样的,这里就不赘述了,关于dynamic_cast的使用问题大家可以看一下这篇文章《深入理解C++中五种强制类型转换的使用场景》,里面有详细的介绍。

4、总结

本文先是介绍了RTTI相关的背景知识,然后引出了C++中两个提供RTTI功能的关键字:typeiddynamic_cast。后面通过对typeid的使用和原理进行分析来说明RTTI机制在C++中究竟是如何实现的。而dynamic_casttypeid获取动态类型的方式是相同的,由于之前已经写过一篇文章介绍dynamic_cast了,所以这里就不赘述了。

最后,如果大家觉得本文写得好的话麻烦点赞收藏关注一下谢谢,也可以关注该专栏,以后会有更多优质文章输出的。