[英]Practical use of dynamic_cast?
我有一個關於dynamic_cast
運算符的非常簡單的問題。 我知道這用於運行時類型識別,即在運行時了解對象類型。 但是根據您的編程經驗,您能否提供一個真實場景,您必須使用此運算符? 沒有使用它有什么困難?
玩具示例
諾亞方舟將作為不同類型動物的容器。 由於方舟本身並不關心猴子,企鵝和蚊子之間的區別,你可以定義一個類Animal
,從中派生出Monkey
, Penguin
和Mosquito
類,並將它們中的每一個存儲為方舟中的Animal
。
一旦洪水結束,諾亞想要將動物分布在地球上,並將它們分配到它們所屬的地方,因此需要更多關於存放在他的方舟中的通用動物的知識。 作為一個例子,他現在可以嘗試dynamic_cast<>
每個動物的Penguin
為了弄清楚哪些動物是企鵝在南極被釋放,哪些不是。
真實的例子
我們實現了一個事件監視框架,其中應用程序將運行時生成的事件存儲在列表中。 事件監視器將遍歷此列表並檢查它們感興趣的特定事件。事件類型是OS級別的事物,例如SYSCALL
, FUNCTIONCALL
和INTERRUPT
。
在這里,我們將所有特定事件存儲在Event
實例的通用列表中。 然后,監視器將遍歷此列表並將他們看到的事件dynamic_cast<>
迭代到他們感興趣的類型。所有其他(引發異常的那些)將被忽略。
問題 :為什么不能為每種事件類型單獨列出一個列表?
答 :您可以這樣做,但它會使系統擴展新事件以及新監視器(聚合多個事件類型)更難,因為每個人都需要知道要檢查的相應列表。
一個典型的用例是訪問者模式 :
struct Element
{
virtual ~Element() { }
void accept(Visitor & v)
{
v.visit(this);
}
};
struct Visitor
{
virtual void visit(Element * e) = 0;
virtual ~Visitor() { }
};
struct RedElement : Element { };
struct BlueElement : Element { };
struct FifthElement : Element { };
struct MyVisitor : Visitor
{
virtual void visit(Element * e)
{
if (RedElement * p = dynamic_cast<RedElement*>(e))
{
// do things specific to Red
}
else if (BlueElement * p = dynamic_cast<BlueElement*>(e))
{
// do things specific to Blue
}
else
{
// error: visitor doesn't know what to do with this element
}
}
};
現在,如果你有一些Element & e;
,你可以制作MyVisitor v;
並說e.accept(v)
。
關鍵設計功能是,如果您修改Element
層次結構,則只需編輯訪問者。 該模式仍然相當復雜,只有在您具有非常穩定的Element
類層次結構時才推薦使用。
想象一下這種情況:你有一個可以讀取和顯示HTML的C ++程序。 你有一個基類HTMLElement
,它有一個純虛方法displayOnScreen
。 您還有一個名為renderHTMLToBitmap
的函數,它將HTML繪制到位圖。 如果每個HTMLElement
都有一個vector<HTMLElement*> children;
,你可以傳遞表示元素<html>
的HTMLElement
。 但是如果一些子類需要特殊處理,比如用於添加CSS的<link>
。 您需要一種方法來了解元素是否是LinkElement
以便您可以將其賦予CSS函數。 要找到它,你可以使用dynamic_cast
。
一般來說, dynamic_cast
和多態的問題在於它並不是非常有效。 當你將vtable添加到混合中時,它只會變得更糟。
當您將虛函數添加到基類時,當它們被調用時,您最終會經歷相當多的函數指針和內存區域。 這永遠不會比ASM call
指令更有效。
編輯:回應安德魯的評論,這是一個新的方法:而不是動態轉換為特定的元素類型( LinkElement
),而是你有一個名為ActionElement
的HTMLElement
另一個抽象子類,用一個不顯示任何內容的函數覆蓋displayOnScreen
,並創建一個新的純虛函數: virtual void doAction() const = 0
。 dynamic_cast
更改為測試ActionElement
並只調用doAction()
。 對於具有虛方法displayOnScreen()
GraphicalElement
,您將擁有相同類型的子類。
編輯2:這是“渲染”方法的樣子:
void render(HTMLElement root) {
for(vector<HTLMElement*>::iterator i = root.children.begin(); i != root.children.end(); i++) {
if(dynamic_cast<ActionElement*>(*i) != NULL) //Is an ActionElement
{
ActionElement* ae = dynamic_cast<ActionElement*>(*i);
ae->doAction();
render(ae);
}
else if(dynamic_cast<GraphicalElement*>(*i) != NULL) //Is a GraphicalElement
{
GraphicalElement* ge = dynamic_cast<GraphicalElement*>(*i);
ge->displayToScreen();
render(ge);
}
else
{
//Error
}
}
}
運算符dynamic_cast
解決了與動態調度(虛函數,訪問者模式等)相同的問題:它允許您根據對象的運行時類型執行不同的操作。
但是,您應該總是更喜歡動態調度,除非您需要的dynamic_cast
數量永遠不會增長。
例如。 你永遠不應該這樣做:
if (auto v = dynamic_cast<Dog*>(animal)) { ... }
else if (auto v = dynamic_cast<Cat*>(animal)) { ... }
...
出於可維護性和性能原因,但您可以這樣做。
for (MenuItem* item: items)
{
if (auto submenu = dynamic_cast<Submenu*>(item))
{
auto items = submenu->items();
draw(context, items, position); // Recursion
...
}
else
{
item->draw_icon();
item->setup_accelerator();
...
}
}
我發現在這種情況下非常有用:你有一個非常特殊的子層次結構,必須單獨處理,這就是dynamic_cast
閃耀的地方。 但現實世界的例子非常少見(菜單示例是我必須處理的事情)。
dynamic_cast 不是虛擬功能的替代品。
dynamic_cast具有非平凡的性能開銷(或者我認為),因為必須遍歷整個類層次結構。
dynamic_cast類似於C#的'is'運算符和舊的COM的QueryInterface。
到目前為止,我發現了一個真正的dynamic_cast用法:
(*)您有多個繼承並且要找到編譯器的目標,編譯器必須上下移動類層次結構以定位目標(如果您願意,可以向下和向上)。 這意味着強制轉換的目標位於與轉換源在層次結構中的位置相關的並行分支中。 我認為沒有其他方法可以做這樣的演員。
在所有其他情況下,您只需使用一些基類虛擬來告訴您您擁有的對象類型,並且只有您將其dynamic_cast到目標類,這樣您就可以使用它的一些非虛擬功能。 理想情況下,應該沒有非虛擬功能,但是,我們生活在現實世界中。
做的事情如下:
if (v = dynamic_cast(...)){} else if (v = dynamic_cast(...)){} else if ...
是一種性能浪費。
應該盡可能避免轉換,因為它基本上是告訴編譯器你知道的更好,這通常是一些較弱的設計決策的標志。
但是,您可能會遇到抽象級別對於1或2個子類來說太高的情況,您可以選擇更改設計或通過使用dynamic_cast檢查子類並在單獨的分支中處理它來解決它。 交易是在以后增加額外的時間和風險與額外的維護問題之間。
在您編寫代碼的大多數情況下,您知道正在使用的實體的類型,您只需使用static_cast,因為它更有效。
您需要動態強制轉換的情況通常在設計中缺乏遠見(根據我的經驗) - 通常在設計人員未能提供枚舉或ID的情況下,您可以在代碼中稍后確定類型。
例如,我已經在多個項目中看到過這種情況:
您可以使用工廠,其中內部邏輯決定用戶想要哪個派生類,而不是用戶明確選擇一個。 在完美的世界中,該工廠返回一個枚舉,它將幫助您識別返回對象的類型,但如果不是,您可能需要使用dynamic_cast測試它為您提供的對象類型。
您的后續問題顯然是:為什么您需要知道您在使用工廠的代碼中使用的對象類型?
在完美的世界中,您不會 - 基類提供的接口足以將所有工廠返回的對象管理到所有必需的范圍。 人們雖然設計不完美。 例如,如果您的工廠創建抽象連接對象,您可能會突然意識到您需要訪問套接字連接對象上的UseSSL標志,但工廠基礎不支持它,並且它與使用它的任何其他類無關。接口。 所以,也許你會檢查你是否在你的邏輯中使用那種類型的派生類,如果你是的話,直接轉換/設置標志。
這很難看,但它並不是一個完美的世界,有時候你沒有時間在工作壓力下完全在現實世界中重構不完美的設計。
Contract Programming和RTTI展示了如何使用dynamic_cast
來允許對象宣傳它們實現的接口。 我們在我的商店里使用它來取代一個相當不透明的元對象系統。 現在我們可以清楚地描述對象的功能,即使在平台被“烘焙”幾個星期/幾個月后由新模塊引入對象(當然,合同需要事先決定)。
dynamic_cast運算符對我非常有用。 我特別將它與Observer模式一起用於事件管理 :
#include <vector>
#include <iostream>
using namespace std;
class Subject; class Observer; class Event;
class Event { public: virtual ~Event() {}; };
class Observer { public: virtual void onEvent(Subject& s, const Event& e) = 0; };
class Subject {
private:
vector<Observer*> m_obs;
public:
void attach(Observer& obs) { m_obs.push_back(& obs); }
public:
void notifyEvent(const Event& evt) {
for (vector<Observer*>::iterator it = m_obs.begin(); it != m_obs.end(); it++) {
if (Observer* const obs = *it) {
obs->onEvent(*this, evt);
}
}
}
};
// Define a model with events that contain data.
class MyModel : public Subject {
public:
class Evt1 : public Event { public: int a; string s; };
class Evt2 : public Event { public: float f; };
};
// Define a first service that processes both events with their data.
class MyService1 : public Observer {
public:
virtual void onEvent(Subject& s, const Event& e) {
if (const MyModel::Evt1* const e1 = dynamic_cast<const MyModel::Evt1*>(& e)) {
cout << "Service1 - event Evt1 received: a = " << e1->a << ", s = " << e1->s << endl;
}
if (const MyModel::Evt2* const e2 = dynamic_cast<const MyModel::Evt2*>(& e)) {
cout << "Service1 - event Evt2 received: f = " << e2->f << endl;
}
}
};
// Define a second service that only deals with the second event.
class MyService2 : public Observer {
public:
virtual void onEvent(Subject& s, const Event& e) {
// Nothing to do with Evt1 in Service2
if (const MyModel::Evt2* const e2 = dynamic_cast<const MyModel::Evt2*>(& e)) {
cout << "Service2 - event Evt2 received: f = " << e2->f << endl;
}
}
};
int main(void) {
MyModel m; MyService1 s1; MyService2 s2;
m.attach(s1); m.attach(s2);
MyModel::Evt1 e1; e1.a = 2; e1.s = "two"; m.notifyEvent(e1);
MyModel::Evt2 e2; e2.f = .2f; m.notifyEvent(e2);
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.