C++ RTTI與4種類型轉換的深入理解

前言

RTTI 是 Run Time Type Information 的縮寫,從字面上來理解就是執行時期的類型信息,其重要作用就是動態判別執行時期的類型。

並不是說這篇文章是RTTI,和用於RTTI的四種類型轉換,而是介紹RTTI,再介紹一下4種類型轉換,因為RTTI有用到其中一種類型轉換,所以相當於兩篇文章寫在一起。

實際上 RTTI 用到的是typeid() 和 dynamic_cast()。

為什麼會有RTTI?

C++是一種靜態類型語言,其數據類型是在編譯期就確定的,不能在運行時更改。然而由於面向對象程序設計中多態性的要求,C++中的指針或引用(Reference)本身的類型,可能與它實際代表(指向或引用)的類型並不一致。有時我們需要將一個多態指針轉換為其實際指向對象的類型,就需要知道運行時的類型信息,這就產生瞭運行時類型識別的要求。

實事求是地講,RTTI是有用的。 但因為一些理論上及方法論上的原因,它破壞瞭面向對象的純潔性。

首先, 它破壞瞭抽象,使一些本來不應該被使用的方法和屬性被不正確地使用。

其次,因為運行時類型的不確定性,它把程序變得更脆弱。

第三點,也是最重要的一點,它使程序缺乏擴展性。 當加入瞭一個新的類型時,你也許需要仔細閱讀你的dynamic_cast的代碼,必要時改動它們,以保證這個新的類型的加入不會導致問題。 而在這個過程中,編譯器將不會給你任何幫助。

總的來說,RTTI 因為它的方法論上的一些缺點,它必須被非常謹慎地使用。 今天面向對象語言的類型系統中的很多東西就是產生於避免RTTI的各種努力。

首先我們來個例子感受一下:

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

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

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

void funcC(Base* p)
{
 Derived* dp = dynamic_cast<Derived*>(p);
 if (dp != NULL) {
  dp->funcB();
 }
 else {
  p->funcA();
 }
};

void funcD(Base* p)
{
 Derived* dp = NULL;
 if (typeid(*p) == typeid(Derived))
 {
  dp = static_cast<Derived*>(p);
  dp->funcB();
 }
 else {
  p->funcA();
 }
}

int main(int argc, char const* argv[])
{
 Base* p = new Derived;
 cout << typeid(p).name() << endl;
 cout << typeid(*p).name() << endl;
 funcD(p);
 funcC(p);
 delete p;

 Base* dp = new Base;
 funcC(dp);
 funcD(dp);
 delete dp;
 return 0;
}

funcC是用dynamic_cast類型轉換是否成功來識別類型的,dynamic_cast操作符將基類類型對象的引用或指針轉化為同一繼承層次中的其他類型的引用或指針。

  • 如果綁定到引用或指針的對象不是目標類型的對象,則dynamic_cast失敗。
  • 如果轉換到指針類型的dynamic_cast失敗,則dynamic_cast的結果是NULL值;
  • 如果轉換到引用類型的dynamic_cast失敗, 則拋出一個bad_cast類型的異常

funcD是用typeid判斷基類地址是否一致的辦法來識別類型的。

typeid

下面我們具體說說 typeid

typeid是C++的關鍵字之一, 等同於sizeof這類operator。 typeid 操作符的返回結果是名為 type_info的標準庫類型的對象的引用,在頭文件typeinfo 中定義。 有兩種形式:

  • typeid(type)
  • typeid(expression)

表達式的類型是類類型,且至少含有一個虛函數,則typeid操作符返回表達式的動態類型,需要在運行時計算,否則返回表達式的靜態類型,在編譯時就可以計算。

C++標準規定瞭其實現必須提供如下四種操作:

  • t1 == t2: 如果兩個對象t1和t2類型相同,則返回true;否則返回false
  • t1 != t2: 如果兩個對象t1和t2類型不同,則返回true;否則返回false
  • t.name(): 返回類型的c-style字符串,類型名字用系統相關的方法產生
  • t1.before(t2): 返回指出t1是否出現在t2之前 的bool值

type_info類提供瞭public虛析構函數,以使用戶能夠用其作為基類。它的默認構造函數和復制構造函數及賦值操作符都定義為private,所以不能定義或復制type_info 類型的對象。 程序中創建type_info對象的唯一方法是使用typeid操作符。 由此可見,如果把typeid看作函數的話,其應該是type_info的友元。 這具體由編譯器的實現所決定,標準隻要求實現為每個類型返回唯一的字符串。

例1: C++裡面的typeid運箕符返回值是什麼?

答: 常量對象的引用。

如果p是基類指針,並且指向一個派生類型的對象,並且基類中有虛函數,那麼typeid(*p)返回p所指向的派生類類型,typeid(p)返回基類類型。

RTTI的實現原理: 通過在虛表中放一個額外的指針,每個新類隻產生一個typeinfo實例,額外指針指向typeinfo, typeid返回對它的一個引用。

static_cast

static_cast < new_type > ( expression )

本來應該先討論dynamic_cast的,因為咱們本來聊的RTTI嘛,但是先瞭解一下static_cast,然後看和dynamic_cast的比較可能更好一點。

使用場景:

  1. 基本數據類型之間的轉換
  2. initializing conversion
int n = static_cast<int>(3.14);
cout << "n = " << n << '\n';
vector<int> v = static_cast<vector<int>>(10);
cout << "v.size() = " << v.size() << '\n';
  1. 類指針或引用向下轉換。
  2. 將類型轉為右值類型,進行move操作,這個在標準庫中有體現(放在下一篇文章中解釋)
vector<int> v = static_cast<vector<int>>(10);
vector<int> v2 = static_cast<vector<int>&&>(v);
cout << "after move, v.size() = " << v.size() << '\n';
cout << v.size() << endl;

子類數組指針向上轉成父類的指針

struct B {
 int m = 0;
 void hello() const {
  cout << "Hello world, this is B!\n";
 }
};
struct D : B {
 void hello() const {
  cout << "Hello world, this is D!\n";
 }
};

D a[10];
B* dp = static_cast<B*>(a);
dp->hello();

枚舉轉換成int or float

enum E { ONE = 1, TWO, THREE };
E e = E::ONE;
int one = static_cast<int>(e);
cout << one << '\n';

int to enum, enum to another emum

enum class E { ONE = 1, TWO, THREE };
enum EU { ONE = 1, TWO, THREE };

E e = E::ONE;
int one = static_cast<int>(e);
E e2 = static_cast<E>(one);
EU eu = static_cast<EU>(e2);

void* to any type

int a = 100;
void* voidp = &a;
int *p = static_cast<int*>(voidp);

註意:

  1. static_cast不能轉換掉expression的const、volatile和__unaligned屬性,
  2. 編譯器隱式執行任何類型轉換都可由static_cast顯示完成

dynamic_cast

dynamic_cast < new-type > ( expression )

接下來是dynamic_cast:

動態映射可以映射到中間層級,將派生類映射到任何一個基類,然後在基類之間可以相互映射。

dynamic_cast實現原理: 先恢復源指針的RTTI信息,再取目標的RTTT信息,比較兩者是否相同,不同取目標類型的基類; 由於它需要檢查一長串基類列表,故動態映射的開銷比typeid大。

dynamic_cast的安全性: 如實現原理所說,dynamic_cast會做一系列的類型檢查,轉換成功會返回目標類型指針,失敗則會返回NULL, 相對於static_cast安全,因為 static_cast即使轉換失敗也不會返回NULL。

例2: 這種情況下 static_cast() 也是安全的。

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

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

int main(int argc, char const* argv[])
{
 Derived *pd = new Derived;
 pd->func();
 Base* pb1 = dynamic_cast<Base*>(pd);
 pb1->func();
 Base* pb2 = static_cast<Base*>(pd);
 pb2->func();
 return 0;
}

pd 指針指向的內存是子類對象,我們知道,繼承子類是包含父類的,相當於在父類的基礎上在添加子類的成員(如果你還不清楚的話,建議你看一下我之前的文章: 虛函數,虛表深度剖析)。 所以pd指針轉成父類指針也是沒問題的,static_cast也一樣安全。

相反,如果指針指向的內存是父類成員,轉成子類指針,dynamic_cast 則會失敗,返回NULL, 但是static_cast不會失敗,強制轉過去瞭,如果此時子類指針訪問父類中不存在,但是子類中存在的成員,則會發生意想不到的問題。

看下面這個例子:

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

class Derived : public Base
{
public:
 virtual void func() { cout << "Derived func" << endl; }
 int m_value = 0;
};

int main(int argc, char const* argv[])
{
 Base *pb = new Base;
 pb->func();
 Derived* pd1 = dynamic_cast<Derived*>(pb);
 if (pd1 != NULL) {
  pd1->func();
 } else {
  cout << "dynamic_cast failed" << endl;
 }
 Derived* pd2 = static_cast<Derived*>(pb);
 pd2->func();
 cout << "m_value: " << pd2->m_value << endl;
 return 0;
}

輸出:

Base func
dynamic_cast failed
Base func  // 父類中也有這個虛函數,所以static_cast轉換調用沒出問題。
m_value: -33686019  // 這裡出問題瞭

對於上行轉換,static_cast 和 dynamic_cast 效果一樣,都安全, 如果隻是單純的向上轉的話,沒必要,直接用虛函數實現就好瞭。

對於下行轉換: 你必須確定要轉換的數據確實是目標類型的數據,即需要註意要轉換的父類類型指針是否真的指向子類對象,如果是,static_cast和dynamic_cast都能成功;如果不是static_cast能返回,但是不安全,可能會出現訪問越界錯誤,而dynamic_cast在運行時類型檢查過程中,判定該過程不能轉換,返回NULL。

const_cast

const_cast < new_type > ( expression )

上面講瞭static_cast是不能去掉 const,而 const_cast是專門用來去掉 const。

而添加const, static_cast 也是可以添加上 const,隻是不能去掉const

看下面一個例子:

const int a = 26;
const int* pa = &a;
//*pa = 3; // 編譯不過,指針常量不能通過指針修改值

int* b = const_cast<int*>(pa); // 把const轉換掉
*b = 3;

cout << "a: " << a << endl; // 26
cout << "*b: " << *b << endl; // 3

a 為 const int 類型,不可修改,pa 為 const int* 類型,不能通過pa指針修改a的值

b 通過 const_cast 轉換掉瞭const,成功修改瞭 a 的值。

有一個問題,為什麼a輸出是26呢?

如果存在const int x = 26; 這種情況,那麼編譯器會認為x是一個編譯期可計算出結果的常量,那麼x就會像宏定義一樣,用到x的地方會被編譯器替換成26。

上述這個例子不建議使用,因為 a 聲明為 const int 類型,實際上是並不希望被修改的,這樣強行修改可能會導致項目裡不可預期的錯誤。

const_cast 的使用場景

如果有一個函數,它的形參是non-const類型變量,而且函數不會對實參的值進行改動,這時我們可以使用類型為const的變量來調用函數。

void func(int* a)
{ }

int main()
{
  const int a = 26;
  const int* pa = &a;
  func(const_cast<int*>(pa));
}

這種情況其實我覺得沒必要,實際上我不想改的話,我形參加 const,把前提推翻不就行瞭,還安全。

定義瞭一個non-const的變量,卻使用瞭一個指向const值的指針來指向它(這不是沒事找事嘛),在程序的某處我們想改變這個變量的值瞭,但手頭隻持有指針,這時const_cast就可以用到瞭

int a = 26;
const int* pa = &a;
// *pa = 1; 編譯不過
int* pa2 = const_cast<int*>(pa);
*pa2 = 1;

reinterpret_cast

reinterpret_cast < new_type > ( expression )

reinterpret_ cast 通常為操作數的位模式提供較低層的重新解釋。

看這個例子:

int n = 9; double d = static_cast<double>(n); 
cout << n << " " << d; // 輸出9 9

int n2 = 9; double d2 = reinterpret_cast<double&>(n2);
cout << n2 << " " << d2; // 輸出9 -9.25596e+61

上面的例子中,我們將一個變址從int 轉換到 double。這些類型的二進制表達式是不同的。要將整數9轉換到雙精度整數9, static_cast需要正確地為雙精度整數d補足比特位。其結果為9.0。

而 reinterpret_cast 的行為卻不同,僅僅是把內存拷貝到目標空間,解釋出來是一個大數。

reinterpret_cast這個操作符被用於的類型轉換的轉換結果幾乎都是未知的。

使用 reinterpret_cast 的代碼很難移植。轉換函數指針的代碼是不可移植的,(C++不保證所有的函數指針都被用一樣的方法表示),在一些情況下這樣的轉換會產生不正確的結果。 所以應該避免轉換函數指針類型,按照C++新思維的話來說,reinterpret_cast 是為瞭映射到一個完全不同類型的意思,這個關鍵詞在我們需要把類型映射回原有類型時用到它。
我們映射到的類型僅僅是為瞭故弄玄虛和其他目的,這是所有映射中最危險的。reinterpret_cast就是一把銳利無比的雙刃劍,除非你處於背水一戰和火燒眉毛的危急時刻,否則絕不能使用。

其實 reinterpret_cast 用法細節還有不少,什麼時候需要用到,再去官方瞭解一下就好瞭,現在糾的太細意義不大。

總結

到此這篇關於C++ RTTI與4種類型轉換的文章就介紹到這瞭,更多相關C++ RTTI與類型轉換內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: