網站首頁 編程語言 正文
當僅有一個指針或引用指向基類型時,利用運行時類型識別(RTTI)可以找到一個對象的動態類型。
運行時類型識別可能被認為是C++中一個”次要“的特征,當程序員在編程過程中陷入非常困難的境地時,實用主義將會幫助他走出困境。正常情況下,程序員需要有意忽略對象的準確類型,而利用虛函數機制實現那個類型正確操作過程。然而,有時知道一個僅含一個基類指針的對象的準確的運行時類型(即多半是派生的類型)是非常有用的。有了此信息,就可以更有效地進行某些特殊情況的操作,或者預防基類接口因無此信息而變得笨拙。
1.運行時類型轉換
通過指針或者引用決定對象運行時類型的一種方法是使用運行時類型轉換(runtime cast),用這種方法可以查證所嘗試進行的轉換正確與否。當要把基類指針類型轉換為派生類時,這個方法非常有用。由于繼承的層次結構的典型描述說基類在派生類之上,所以這種類型轉換也成為向下類型轉換(downcast)。
請看下面的類層次結構:
在下面的程序代碼中,Investment類中有一個其他類沒有的額外操作,所以能夠運行時知道Security指針是否引用了Investment對象是很重要的。為了實現檢查運行時的類型轉換,每個類都持有一個整數標識符,以便可以與層次結構中其他的類區別開來。
#include <iostream>
#include <vector>
#include "../purge.h"
using namespace std;
class Security {
protected:
enum { BASEID = 0 };
public:
virtual ~Security() {}
virtual bool isA(int id) { return (id == BASEID); }
};
class Stock : public Security {
typedef Security Super;
protected:
enum { OFFSET = 1, TYPEID = BASEID + OFFSET };
public:
bool isA(int id) {
return id == TYPEID || Super::isA(id);
}
static Stock* dynacast(Security* s) {
return (s->isA(TYPEID)) ? static_cast<Stock*>(s) : 0;
}
};
class Bond : public Security {
typedef Security Super;
protected:
enum { OFFSET = 2, TYPEID = BASEID + OFFSET };
public:
bool isA(int id) {
return id == TYPEID || Super::isA(id);
}
static Bond* dynacast(Security* s) {
return (s->isA(TYPEID)) ? static_cast<Bond*>(s) : 0;
}
};
class Investment : public Security {
typedef Security Super;
protected:
enum { OFFSET = 3, TYPEID = BASEID + OFFSET };
public:
bool isA(int id) {
return id == TYPEID || Super::isA(id);
}
static Investment* dynacast(Security* s) {
return (s->isA(TYPEID)) ?
static_cast<Investment*>(s) : 0;
}
void special() {
cout << "special Investment function" << endl;
}
};
class Metal : public Investment {
typedef Investment Super;
protected:
enum { OFFSET = 4, TYPEID = BASEID + OFFSET };
public:
bool isA(int id) {
return id == TYPEID || Super::isA(id);
}
static Metal* dynacast(Security* s) {
return (s->isA(TYPEID)) ? static_cast<Metal*>(s) : 0;
}
};
int main() {
vector<Security*> portfolio;
portfolio.push_back(new Metal);
portfolio.push_back(new Investment);
portfolio.push_back(new Bond);
portfolio.push_back(new Stock);
for(vector<Security*>::iterator it = portfolio.begin();
it != portfolio.end(); ++it) {
Investment* cm = Investment::dynacast(*it);
if(cm)
cm->special();
else
cout << "not an Investment" << endl;
}
cout << "cast from intermediate pointer:" << endl;
Security* sp = new Metal;
Investment* cp = Investment::dynacast(sp);
if(cp) cout << " it's an Investment" << endl;
Metal* mp = Metal::dynacast(sp);
if(mp) cout << " it's a Metal too!" << endl;
purge(portfolio);
} ///:~
多態的isA()函數檢查其參數是否與它的類型參數(id)相容,就意味著或者id與對象的typeID準確地匹配,或者與對象的祖先之一的類型匹配(因為在這種情況下調用Super::isA())。函數dynacast()在每個類中都是靜態的,dynacast()為其指針參數調用isA()來檢查類型轉換是否有效。
借助dynamic_cast操作符,C++提供這樣一個可檢查的類型轉換。使用dynamic_cast對前面的程序例子進行重寫,就得到下面的程序:
//: C08:Security.h
#ifndef SECURITY_H
#define SECURITY_H
#include <iostream>
class Security {
public:
virtual ~Security() {}
};
class Stock : public Security {};
class Bond : public Security {};
class Investment : public Security {
public:
void special() {
std::cout << "special Investment function" <<std::endl;
}
};
class Metal : public Investment {};
#endif // SECURITY_H ///:~
// Uses RTTI's dynamic_cast.
#include <vector>
#include "../purge.h"
#include "Security.h"
using namespace std;
int main() {
vector<Security*> portfolio;
portfolio.push_back(new Metal);
portfolio.push_back(new Investment);
portfolio.push_back(new Bond);
portfolio.push_back(new Stock);
for(vector<Security*>::iterator it =
portfolio.begin();
it != portfolio.end(); ++it) {
Investment* cm = dynamic_cast<Investment*>(*it);
if(cm)
cm->special();
else
cout << "not a Investment" << endl;
}
cout << "cast from intermediate pointer:" << endl;
Security* sp = new Metal;
Investment* cp = dynamic_cast<Investment*>(sp);
if(cp) cout << " it's an Investment" << endl;
Metal* mp = dynamic_cast<Metal*>(sp);
if(mp) cout << " it's a Metal too!" << endl;
purge(portfolio);
} ///:~
由于原來例子中大部分的代碼開銷用在了類型轉換檢查上,所以這個例子就變得如此之短。如果想要安全地進行向下類型轉換,dynamic_cast要求使用的目標類型是多態的(polymorphic)。這就要求該類必須至少有一個虛函數。幸運的是,Security基類有一個虛析構函數,所以這里不需要再創建一個額外的函數去做這項工作。因為dynamic_cast在程序運行時使用了虛函數表,所以比其他新式風格的類型轉換操作來說它的代價更高。
用引用而非指針同樣也可以使用dynamic_cast,但是由于沒有諸如空引用這樣的情況,這就需要采用其他方法來了解類型轉換是否失敗。這個”其他方法“就是捕獲bad_cast異常,如下所示:
#include <typeinfo>
#include "Security.h"
using namespace std;
int main() {
Metal m;
Security& s = m;
try {
Investment& c = dynamic_cast<Investment&>(s);
cout << "It's an Investment" << endl;
} catch(bad_cast&) {
cout << "s is not an Investment type" << endl;
}
try {
Bond& b = dynamic_cast<Bond&>(s);
cout << "It's a Bond" << endl;
} catch(bad_cast&) {
cout << "It's not a Bond type" << endl;
}
} ///:~
2.typeid操作符
獲得有關一個對象運行時信息的另一個方法,就是typeid操作符來完成。這種操作符返回一個type_info類的對象,該對象給出與其應用有關的對象類型的信息。如果該對象的類型是多態的,它將給出那個應用(動態類型(dynamic type))的大部分派生類信息;否則,它將給出靜態類型信息。typeid操作符的一個用途是獲得一個對象的動態類型的名稱,例如const char * ,就像在下面例子中可以看到的:
#include <iostream>
#include <typeinfo>
using namespace std;
struct PolyBase { virtual ~PolyBase() {} };
struct PolyDer : PolyBase { PolyDer() {} };
struct NonPolyBase {};
struct NonPolyDer : NonPolyBase { NonPolyDer(int) {} };
int main() {
// Test polymorphic Types
const PolyDer pd;
const PolyBase* ppb = &pd;
cout << typeid(ppb).name() << endl;
cout << typeid(*ppb).name() << endl;
cout << boolalpha << (typeid(*ppb) == typeid(pd))
<< endl;
cout << (typeid(PolyDer) == typeid(const PolyDer))
<< endl;
// Test non-polymorphic Types
const NonPolyDer npd(1);
const NonPolyBase* nppb = &npd;
cout << typeid(nppb).name() << endl;
cout << typeid(*nppb).name() << endl;
cout << (typeid(*nppb) == typeid(npd)) << endl;
// Test a built-in type
int i;
cout << typeid(i).name() << endl;
} ///:~
這是使用一個特定編譯器的程序的輸出是:
struct PolyBase const *
struct Polyder
true
true
struct NonPolyBase const *
struct NonPolyBase
false
int
因為ppb是一個指針,所以輸出的第1行是他的靜態類型。為了在程序中得到RTTI的結果,需要檢查指針或引用目標對象,這在第2行說明。需要注意的是,RTTI忽略了頂層的const和volatile限定符。借助非多態類型,正好可以獲得靜態類型(該指針本身的類型)。正如讀者所見,這里也支持內置類型。
2.1類型轉換到中間層次類型
#include <cassert>
#include <typeinfo>
using namespace std;
class B1 {
public:
virtual ~B1() {}
};
class B2 {
public:
virtual ~B2() {}
};
class MI : public B1, public B2 {};
class Mi2 : public MI {};
int main() {
B2* b2 = new Mi2;
Mi2* mi2 = dynamic_cast<Mi2*>(b2);
MI* mi = dynamic_cast<MI*>(b2);
B1* b1 = dynamic_cast<B1*>(b2);
assert(typeid(b2) != typeid(Mi2*));
assert(typeid(b2) == typeid(B2*));
delete b2;
} ///:~
如果創建一個Mi2對象并將它向上類型轉換到該繼承層次結構的根(在這種情況下,選擇兩個可能的根中的一個),可以成功地使dynamic_cast回退至兩個派生層MI或Mi2中的任何一個。
甚至可以從一個根到另一個根進行類型轉換:
B1* b1 = dynamic_cast<B1*>(b2);
這也是成功的,因為B2實際上指向一個Mi2對象,該Mi2對象含有一個B1類型的子對象。
2.2void型指針
RTTI僅僅為完整的類型工作,這就意味著當使用typeid時,所有的類型信息必須是可利用的。特別是,它不能與void型指針一起工作:
//!#include <iostream>
#include <typeinfo>
using namespace std;
class Stimpy {
public:
virtual void happy() {}
virtual void joy() {}
virtual ~Stimpy() {}
};
int main() {
void* v = new Stimpy;
// Error:
//! Stimpy* s = dynamic_cast<Stimpy*>(v);
// Error:
//! cout << typeid(*v).name() << endl;
} ///:~
一個void* 真是的意思是”無類型信息“。
2.3運用帶模板的RTTI
因為所有的類模板所做的工作就是產生類,所以類模板可以很好地與RTTI一起工作。RTTI提供了一條方便的途徑來獲得對象所在類的名稱。下面的示例打印出構造函數和析構函數的調用順序:
#include <iostream>
#include <typeinfo>
using namespace std;
template<int id> class Announce {
public:
Announce() {
cout << typeid(*this).name() << " constructor" << endl;
}
~Announce() {
cout << typeid(*this).name() << " destructor" << endl;
}
};
class X : public Announce<0> {
Announce<1> m1;
Announce<2> m2;
public:
X() { cout << "X::X()" << endl; }
~X() { cout << "X::~X()" << endl; }
};
int main() { X x; } ///:~
在構造函數內和析構函數內部,RTTI信息產生打印的類名。類X利用繼承和組合兩個方式創建一個類。輸出如下:
Announce<0> constructor
Announce<1> constructor
Announce<2> constructor
X::X()
X::~X()
Announce<2> destructor
Announce<1> destructor
Announce<0> destructor
當然,可能會得到不同的結果,這取決于編譯器如何表示它的name()信息。
3.多重繼承
RTTI機制必須正確地處理多重繼承的所有復雜性,包括虛基類virtual:
#include <iostream>
#include <typeinfo>
using namespace std;
class BB {
public:
virtual void f() {}
virtual ~BB() {}
};
class B1 : virtual public BB {};
class B2 : virtual public BB {};
class MI : public B1, public B2 {};
int main() {
BB* bbp = new MI; // Upcast
// Proper name detection:
cout << typeid(*bbp).name() << endl;
// Dynamic_cast works properly:
MI* mip = dynamic_cast<MI*>(bbp);
// Can't force old-style cast:
//! MI* mip2 = (MI*)bbp; // Compile error
} ///:~
typeid()操作符正確地檢測出實際對象的名字,即便它采用virtual基類指針完成這個任務的,dynamic_cast也正確地進行工作。但實際上,編譯器不允許程序員用以前的方法嘗試強制進行類型轉換:
MI* mip2 = (MI*)bbp; // Compile error
編譯器知道這樣做絕不是正確的方法,因此需要程序員使用dynamic_cast。
4.合理使用RTTI
垃圾再生器
為了進一步地舉例說明RTTI的實際用途,下面的程序模擬了一個垃圾再生器。不同種類的”垃圾“被插入一個容器中,然后根據它們的動態類型進行分類。
// Describing trash.
#ifndef TRASH_H
#define TRASH_H
#include <iostream>
class Trash {
float _weight;
public:
Trash(float wt) : _weight(wt) {}
virtual float value() const = 0;
float weight() const { return _weight; }
virtual ~Trash() {
std::cout << "~Trash()" << std::endl;
}
};
class Aluminum : public Trash {
static float val;
public:
Aluminum(float wt) : Trash(wt) {}
float value() const { return val; }
static void value(float newval) {
val = newval;
}
};
class Paper : public Trash {
static float val;
public:
Paper(float wt) : Trash(wt) {}
float value() const { return val; }
static void value(float newval) {
val = newval;
}
};
class Glass : public Trash {
static float val;
public:
Glass(float wt) : Trash(wt) {}
float value() const { return val; }
static void value(float newval) {
val = newval;
}
};
#endif // TRASH_H ///:~
用來表示垃圾類型單價的static值定義在實現文件中:
// A Trash Recycler.
#include "Trash.h"
float Aluminum::val = 1.67;
float Paper::val = 0.10;
float Glass::val = 0.23;
///:~
sunValue()模板從頭到尾對一個容器進行迭代,顯示并計算結果:
//{L} Trash
// A Trash Recycler.
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <typeinfo>
#include <vector>
#include "Trash.h"
#include "../purge.h"
using namespace std;
// Sums up the value of the Trash in a bin:
template<class Container>
void sumValue(Container& bin, ostream& os) {
typename Container::iterator tally = bin.begin();
float val = 0;
while(tally != bin.end()) {
val += (*tally)->weight() * (*tally)->value();
os << "weight of " << typeid(**tally).name()
<< " = " << (*tally)->weight() << endl;
++tally;
}
os << "Total value = " << val << endl;
}
int main() {
srand(time(0)); // Seed the random number generator
vector<Trash*> bin;
// Fill up the Trash bin:
for(int i = 0; i < 30; i++)
switch(rand() % 3) {
case 0 :
bin.push_back(new Aluminum((rand() % 1000)/10.0));
break;
case 1 :
bin.push_back(new Paper((rand() % 1000)/10.0));
break;
case 2 :
bin.push_back(new Glass((rand() % 1000)/10.0));
break;
}
// Note: bins hold exact type of object, not base type:
vector<Glass*> glassBin;
vector<Paper*> paperBin;
vector<Aluminum*> alumBin;
vector<Trash*>::iterator sorter = bin.begin();
// Sort the Trash:
while(sorter != bin.end()) {
Aluminum* ap = dynamic_cast<Aluminum*>(*sorter);
Paper* pp = dynamic_cast<Paper*>(*sorter);
Glass* gp = dynamic_cast<Glass*>(*sorter);
if(ap) alumBin.push_back(ap);
else if(pp) paperBin.push_back(pp);
else if(gp) glassBin.push_back(gp);
++sorter;
}
sumValue(alumBin, cout);
sumValue(paperBin, cout);
sumValue(glassBin, cout);
sumValue(bin, cout);
purge(bin);
} ///:~
因為垃圾被不加分類地投入到一個容器,這樣一來,垃圾的所有信息就”丟失“了。但是,為了稍后適當地對廢料進行分類,具體類型信息必須恢復,這將用到RTTI。
可以通過使用map來改進這種解決方案,該map將指向type_info對象的指針與一個包含Trash指針的vector關聯起來。因為映像需要一個能識別排序的判定函數,這里提供了一個名為TInfoLess的結構,它調用type_info::before()。注意,這里必須對sumValue()進行不同的定義。
//{L} Trash
// Recyling with a map.
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <map>
#include <typeinfo>
#include <utility>
#include <vector>
#include "Trash.h"
#include "../purge.h"
using namespace std;
// Comparator for type_info pointers
struct TInfoLess {
bool operator()(const type_info* t1, const type_info* t2)
const { return t1->before(*t2); }
};
typedef map<const type_info*, vector<Trash*>, TInfoLess>
TrashMap;
// Sums up the value of the Trash in a bin:
void sumValue(const TrashMap::value_type& p, ostream& os) {
vector<Trash*>::const_iterator tally = p.second.begin();
float val = 0;
while(tally != p.second.end()) {
val += (*tally)->weight() * (*tally)->value();
os << "weight of "
<< p.first->name() // type_info::name()
<< " = " << (*tally)->weight() << endl;
++tally;
}
os << "Total value = " << val << endl;
}
int main() {
srand(time(0)); // Seed the random number generator
TrashMap bin;
// Fill up the Trash bin:
for(int i = 0; i < 30; i++) {
Trash* tp;
switch(rand() % 3) {
case 0 :
tp = new Aluminum((rand() % 1000)/10.0);
break;
case 1 :
tp = new Paper((rand() % 1000)/10.0);
break;
case 2 :
tp = new Glass((rand() % 1000)/10.0);
break;
}
bin[&typeid(*tp)].push_back(tp);
}
// Print sorted results
for(TrashMap::iterator p = bin.begin();
p != bin.end(); ++p) {
sumValue(*p, cout);
purge(p->second);
}
} ///:~
為了直接調用type_info::name(),我們在這里修改了sunValue(),因為作為TrashMap::value_type對的第1個成員,type_info對象現在是可獲得的。這樣就避免了為了獲得正在處理的Trash的類型名而額外調用typeid,而這在該程序的以前版本中卻是必須做的。
5.RTTI的機制和開銷
實現RTTI典型的方法是,通過在類的虛函數表中放置一個附加的指針。這個指針指向那個特別類型的type_info結構。typeid()表達式的結果非常簡單:虛函數表指針取得type_info指針,并且產生一個對type_info結構的引用。因為這正好是一個雙指針的解析操作,這是一個代價為常量時間的操作。
6.小結
盡管通常情況下會為一個指向其基類的指針進行向上類型轉換,然后再使用那個基類的通用接口(通過虛函數),但是如果知道一個由基類指針指向的對象的動態類型,有時候根據獲得的這些信息進行相關處理可能會使事情變得更加有效,而這些正是RTTI所提供的。大部分通常的誤用來自一些程序員,這些誤用是由于他們不理解虛函數而實采用RTTI來做類型檢查的編碼所造成的。C++的基本原理似乎提供了對違反類型的定義規則和完整性的情況進行監督和糾正的強有力的工具和保護,但是如果有誰想故意誤用或回避某一語言的特征,那么將沒有人可以組織他這樣做。
原文鏈接:https://blog.csdn.net/wbxzgbnzcl/article/details/127175103
相關推薦
- 2022-04-21 C語言中const和指針的秘密你知道嗎_C 語言
- 2022-07-08 C語言完整實現12種排序算法(小結)_C 語言
- 2023-03-18 go?sync.Map基本原理深入解析_Golang
- 2022-10-14 linux【centos 7】 yum 安裝 tesseract 4.1
- 2022-08-24 K8S之StatefulSet有狀態服務詳解_云其它
- 2022-02-14 taro將頁面滾動到指定位置
- 2022-04-21 Android?App頁面滑動標題欄顏色漸變詳解_Android
- 2022-04-02 jquery實現邊框特效_jquery
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支