天天看點

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部落格

        有問題請留言,歡迎轉發,請勿拷貝!

繼續閱讀