天天看点

C++类型预断——RTTI

        C++没有类似 Java 语言的 instanceof 的类型预断,有什么方法可以做到类似 java 的类型预断功能呢?和Java相比,C++要想获得运行时类型信息,只能通过 RTTI (Run Time Type Identification)机制,并且C++最终生成的代码是直接与机器相关的(也就是不同编译器实现RTTI的方式不一样,C++标准只是做了约定)。

        RTTI 提供了两个操作符 :typeid 和 dynamic_cast

        typeid:返回指针和引用所指的实际类型

        dynamic_cast:将基类类型的指针或引用安全地转换为其派生类类型的指针或引用

        C++的动态多态是由虚函数实现的,对于多态性的对象,无法在程序编译阶段确定对象的类型。当类中含有虚函数时,其基类的指针就可以指向任何派生类的对象,这时就有可能不知道基类指针到底指向的是哪个对象的情况,类型的确定要在运行时利用运行时类型标识做出。

1、typeid

        为了获得一个对象的类型可以使用 typeid 操作,该操作返回一个 type_info 类对象的引用,type_info 类的定义在文件<typeinfo>中。

        这里先介绍一下type_info源码:  

class type_info
  {
  public:
    /** Destructor first. Being the first non-inline virtual function, this
     *  controls in which translation unit the vtable is emitted. The
     *  compiler makes use of that information to know where to emit
     *  the runtime-mandated type_info structures in the new-abi.  */
    virtual ~type_info();

    /** Returns an @e implementation-defined byte string; this is not
     *  portable between compilers!  */
    const char* name() const _GLIBCXX_NOEXCEPT
    { return __name[0] == '*' ? __name + 1 : __name; }

#if !__GXX_TYPEINFO_EQUALITY_INLINE
    // In old abi, or when weak symbols are not supported, there can
    // be multiple instances of a type_info object for one
    // type. Uniqueness must use the _name value, not object address.
    bool before(const type_info& __arg) const _GLIBCXX_NOEXCEPT;
    bool operator==(const type_info& __arg) const _GLIBCXX_NOEXCEPT;
#else
  #if !__GXX_MERGED_TYPEINFO_NAMES
    /** Returns true if @c *this precedes @c __arg in the implementation's
     *  collation order.  */
    // Even with the new abi, on systems that support dlopen
    // we can run into cases where type_info names aren't merged,
    // so we still need to do string comparison.
    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
    // On some targets we can rely on type_info's NTBS being unique,
    // and therefore address comparisons are sufficient.
    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

#if __cpp_impl_three_way_comparison < 201907L
    bool operator!=(const type_info& __arg) const _GLIBCXX_NOEXCEPT
    { return !operator==(__arg); }
#endif

#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

    // Return true if this is a pointer type of some kind
    virtual bool __is_pointer_p() const;

    // Return true if this is a function type
    virtual bool __is_function_p() const;

    // Try and catch a thrown type. Store an adjusted pointer to the
    // caught type in THR_OBJ. If THR_TYPE is not a pointer type, then
    // THR_OBJ points to the thrown object. If THR_TYPE is a pointer
    // type, then THR_OBJ is the pointer itself. OUTER indicates the
    // number of outer pointers, and whether they were const
    // qualified.
    virtual bool __do_catch(const type_info *__thr_type, void **__thr_obj,
			    unsigned __outer) const;

    // Internally used during catch matching
    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:
    /// Assigning type_info is not supported.
    type_info& operator=(const type_info&);
    type_info(const type_info&);
  };
           

         可以看到 type_info 类的构造函数/拷贝构造都为私有函数,也就是不允许用户自己来创建type_info类对象。该类常用来判断类型的方法就是 operator()== 和 operator()!= 。

        下面测试代码演示怎么使用 typeid ,注意:必须借助 虚函数表 来实现类型推断!

#include <iostream>
#include <functional>
using namespace std;

class Base{
public:
    virtual 
	void test(){
		cout << "Base::test" << endl;
	}
};

class Derived : public Base{
public:
    void test(){
		cout << "Derived::test" << endl;
	}

	virtual 
	~Derived(){
		cout << "Derived::~Derived" << endl;
	}
};


int main()
{
    Base base;
    Derived derive;
	Derived derive2;
    
	Base* pBase = new Base();
	Base* pBase2 = new Derived();
	Derived* pDerive = new Derived();
	
    //base和derive类型不同,返回false
    cout << "typeid(base) == typeid(derive) : " << (typeid(base) == typeid(derive)) << endl;

	//derive和derive2类型相同,返回true
    cout << "typeid(derive2) == typeid(derive) : " << (typeid(derive2) == typeid(derive)) << endl;
	
	//base和derive类型不同,返回false
    cout << "typeid(&base) == typeid(&derive) : " << (typeid(&base) == typeid(&derive)) << endl;
	
	//derive和derive2类型相同,返回true
    cout << "typeid(&derive2) == typeid(&derive) : " << (typeid(&derive2) == typeid(&derive)) << endl;

	//pBase和pDerive指向对象类型不同,返回false
	cout << "typeid(pBase) == typeid(pDerive) : " << (typeid(pBase) == typeid(pDerive)) << endl;
	
	//pBase2和pDerive指向对象类型相同,返回true
    cout << "typeid(pBase2) == typeid(pDerive) : " << (typeid(*pBase2) == typeid(*pDerive)) << endl;

    return 0;
}
           

        大家可以测试一下,将 virtual 关键字注释掉再试试,类型推断结果会不一样!以下为output信息:

有虚函数的情况:

typeid(base) == typeid(derive) : 0

typeid(derive2) == typeid(derive) : 1

typeid(&base) == typeid(&derive) : 0

typeid(&derive2) == typeid(&derive) : 1

typeid(pBase) == typeid(pDerive) : 0

typeid(pBase2) == typeid(pDerive) : 1

没有虚函数的情况:

typeid(base) == typeid(derive) : 0

typeid(derive2) == typeid(derive) : 1

typeid(&base) == typeid(&derive) : 0

typeid(&derive2) == typeid(&derive) : 1

typeid(pBase) == typeid(pDerive) : 0

typeid(pBase2) == typeid(pDerive) : 0

        为什么会这样呢???

        前面咱们是不是讲到 virtual 是实现动态多态的手段,如果没有虚函数表,那么就可以在编译时根据操作内容推导出类型信息,以下静态类型都能在编译阶段确定类型信息:

  1. 类型名(例如:int、Base)
  2. 一个基本类型的变量(例如:2、3.0f)
  3. 一个具体的对象(例如:Base base;)
  4. 一个指向无虚函数表的类对象的指针

        所以,当你对 typeid(pBase) 和 typeid(pBase2) 进行类型推导时,由于Base类中并不存在虚函数表,最终实际上和 typeid(Base*) 的效果一样!也就是在编译时就能够确定操作数的类型!

        那么 typeid 是怎么动态推导类型的呢?那肯定是和咱们的虚函数表有关系!前面咱们讲过,typeid返回的是type_info类对象的引用,也就是只要是同一个类的实例,typeid 的返回值应该都是相同的!

//typeid(pBase2) 和 typeid(pDerive) 返回地址相同
    cout << "typeid(pBase2) = " << &typeid(*pBase2) <<  " typeid(pDerive) = "<< &typeid(*pDerive) << endl;
           

        在main函数中添加以上测试代码,(注意将 Base 类中的 virtual 关键字注释取消,因为咱们要测试动态类型推导时的返回地址),此时,测试代码输出的日志信息为:

typeid(pBase2) = 0x564006a8ad20 typeid(pDerive) = 0x564006a8ad20

typeid 返回的地址相同,所以你去比较这两个对象的类型肯定也是相同的!

注意:此时你注释掉virtual看看,是否还相同呢?想想为什么?

         typeid 的原理咱们先讲到这,如果你是C++编译器开发者,如果让你去实现RTTI技术,你如何去确定一个对象的真实类型呢?

        首先,每个类在编译之后应该有一个 type_info 数据结构来标识这个类的唯一性,这个 type_info 数据应该是确定的并在编译时就创建出来了。

        其次,typeid 的实现;显示 typeid 的功能无非考虑两种情况:

  1.  静态:直接根据操作数(例如int、无虚函数表的类对象等)找到对应类型的 type_info 数据即可。
  2.  动态:此时想要返回对象的真实类型只能够借助虚函数表了,那么,虚函数表中应该能找到真实类型的 type_info 数据的入口(类似虚函数表存储的是虚函数的地址,这里应该也是存储的是指向 type_info 数据的地址)。

        ok,讲到这里,我们就需要深入挖掘虚函数表相关的知识,那么你要先补充一下虚函数表相关知识。typeid 的动态类型推导与虚函数表的关系,咱们放到下一篇在来讲!

        Tips:可以先看看我的new[]/delete[],原理相通!

2、dynamic_cast

        dynamic_cast运算符将一个指向基类的指针转换成指向派生类的指针;如果失败,返回空指针。注意,主要用来进行向上转换,也就是从父类指针转换成子类指针(用来实现c++多态的),因为子类指针是不需要进行转换就可以被赋值给父类指针的!

        好了嘛,既然是要实现多态,那么这里就必须借助虚函数表!也就是dynamic_cast在操作无虚函数表的对象时是不允许的!(会抛异常/编译错误)

        添加以下测试代码:

//将父类指针转为子类指针
	auto p1 = dynamic_cast<Derived*>(pBase);
	
	//将指向子类的父类指针转为子类指针
	auto p2 = dynamic_cast<Derived*>(pBase2);
	
	cout << "p1 = " << p1 << " p2 = " << p2 << endl;
    
           

日志输出:

p1 = 0 p2 = 0x556f565f4ed0

        至此,咱们就讲解完了C++中RTTI技术中两个操作符的使用方式和原理!后面一篇咱们再对typeid和虚函数的关系做更进一步讲解。

        后一篇地址:typeid 和虚函数_master-计算机科学专栏-CSDN博客

        有问题请留言,欢迎转发,请勿拷贝!

继续阅读