我有一个关于dynamic_cast
运营商的非常简单的问题。我知道这是用于运行时类型识别,即在运行时了解对象类型。但是根据您的编程经验,您能否给出一个必须使用此运算符的真实场景?不使用它有什么困难?
9 回答
玩具示例
挪亚方舟应作为不同类型动物的容器。由于方舟本身并不关心猴子、企鹅和蚊子之间的区别,因此您定义了一个类Animal
,从中派生出类Monkey
,Penguin
和Mosquito
,并将它们中的每一个存储为Animal
方舟中的 。
一旦洪水结束,诺亚想将动物分布在地球上它们所属的地方,因此需要更多关于储存在他方舟中的通用动物的知识。举个例子,他现在可以尝试对dynamic_cast<>
每只动物进行一次测试Penguin
,以确定哪些动物是要在南极放归的企鹅,哪些不是。
现实生活中的例子
我们实现了一个事件监控框架,应用程序将运行时生成的事件存储在一个列表中。事件监视器将遍历此列表并检查他们感兴趣的特定事件。事件类型是操作系统级别的事物,例如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
s 时才推荐使用。
想象一下这种情况:您有一个读取和显示 HTML 的 C++ 程序。您有一个HTMLElement
具有纯虚拟方法的基类displayOnScreen
。您还有一个名为 的函数renderHTMLToBitmap
,它将 HTML 绘制为位图。如果每个HTMLElement
都有一个vector<HTMLElement*> children;
,你可以只传递HTMLElement
代表元素<html>
。但是如果一些子类需要特殊处理,比如<link>
添加 CSS。您需要一种方法来了解元素是否为 a LinkElement
,以便将其提供给 CSS 函数。要找出答案,您可以使用dynamic_cast
.
一般来说,多态性的问题dynamic_cast
在于它的效率不是很高。当您将 vtables 添加到组合中时,情况只会变得更糟。
当您将虚函数添加到基类时,当它们被调用时,您实际上最终会经历相当多的函数指针和内存区域层。这永远不会比 ASMcall
指令更有效。
LinkElement
编辑:为了回应AndrewHTMLElement
的评论,这里有一种新方法:不是动态转换为特定元素ActionElement
类型displayOnScreen
( : virtual void doAction() const = 0
. 更改为dynamic_cast
测试ActionElement
并仅调用doAction()
. 您将拥有GraphicalElement
与虚拟方法相同的子类displayOnScreen()
。
编辑 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
}
}
}
Operatordynamic_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 的一个真正
用途:
(*)您有多重继承,并且要定位强制转换的目标,编译器必须上下遍历类层次结构以定位目标(或者如果您愿意,可以上下移动) . 这意味着演员表的目标与演员表源在层次结构中的位置相关。我认为没有其他方法可以做这样的演员。
在所有其他情况下,您只需使用一些基类 virtual 来告诉您您拥有什么类型的对象,然后您才可以将其 dynamic_cast 为目标类,以便您可以使用它的一些非虚拟功能。理想情况下不应该有非虚拟功能,但到底是什么,我们生活在现实世界中。
执行以下操作:
if (v = dynamic_cast(...)){} else if (v = dynamic_cast(...)){} else if ...
是一种性能浪费。
应该尽可能避免强制转换,因为它基本上是在告诉编译器你知道得更好,这通常是一些较弱的设计决策的标志。
但是,您可能会遇到抽象级别对于 1 个或 2 个子类来说有点太高的情况,您可以选择更改设计或通过使用 dynamic_cast 检查子类并在单独的分支中处理它来解决它。权衡是在现在增加额外的时间和风险之间,以应对以后的额外维护问题。
在大多数情况下,您编写的代码都知道您正在使用的实体的类型,您只需使用 static_cast,因为它更有效。
您需要动态转换的情况(根据我的经验)通常是由于设计中缺乏远见而出现的 - 通常是设计人员无法提供允许您稍后在代码中确定类型的枚举或 ID。
例如,我已经在多个项目中看到过这种情况:
您可以使用工厂,其中内部逻辑决定用户想要哪个派生类,而不是用户明确选择一个。理想情况下,该工厂返回一个枚举,该枚举将帮助您识别返回对象的类型,但如果不是,您可能需要使用 dynamic_cast 测试它为您提供的对象类型。
您的后续问题显然是:为什么您需要知道您在使用工厂的代码中使用的对象类型?
在一个完美的世界里,你不会 - 基类提供的接口足以在所有需要的范围内管理所有工厂返回的对象。然而,人们的设计并不完美。例如,如果您的工厂创建了抽象连接对象,您可能会突然意识到您需要访问套接字连接对象上的 UseSSL 标志,但工厂基础不支持它,并且它与使用界面。所以,也许你会检查你是否在你的逻辑中使用了那种类型的派生类,如果你是的话,直接转换/设置标志。
它很丑,但它不是一个完美的世界,有时你在工作压力下没有时间在现实世界中完全重构一个不完美的设计。
合同编程和 RTTI展示了如何使用dynamic_cast
来允许对象公布它们实现的接口。我们在我的商店中使用它来替换一个相当不透明的元对象系统。现在我们可以清楚地描述对象的功能,即使对象是在平台“烘烤”几周/几个月后由新模块引入的(当然,合同需要事先确定)。
dynamic_cast 运算符对我非常有用。我特别将它与观察者模式一起用于事件管理:
#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);
}