[英]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.