是否通过继承层次结构dynamic_casting不良做法

Is dynamic_casting through inheritance hierarchy bad practice?

本文关键字:casting 不良 dynamic 继承 层次结构 是否      更新时间:2023-10-16

我得到了以下数据结构:

class Element {
  std::string getType();
  std::string getId();
  virtual std::vector<Element*> getChildren();
}
class A : public Element {
  void addA(const A *a);
  void addB(const B *b);
  void addC(const C *c);
  std::vector<Element*> getChildren();
}
class B : public Element {
  void addB(const B *b);
  void addC(const C *c);
  std::vector<Element*> getChildren();
}
class C : public Element {
  int someActualValue;
}
/* The classes also have some kind of container to store the pointers and
 * child elements. But let's keep the code short. */

数据结构用于绘制无环有向图。C 类充当包含代数任务实际数据的"叶子"。A和B包含其他信息,如名称,类型,规则,我最喜欢的颜色和天气预报。

我想对一个功能进行编程,其中会弹出一个窗口,您可以在已经存在的结构中导航。在途中,我想用一些漂亮的流程图显示用户所采用的路径,可以单击该流程图以返回到层次结构中。基于当前访问的图形节点(可以是A,B或C),必须计算和显示一些信息。

我想我可以制作一个 Element* 类型的 std::vector,并使用最后一项作为我使用的活动元素。我认为这是一种非常好的方法,因为它利用了已经存在的继承,并使我需要的代码非常小。

但是我有很多这样的情况:

Element* currentElement;
void addToCurrentElement(const C *c) {
  if(A *a = dynamic_cast<A*>(currentElement)) {
    //doSomething, if not, check if currentElement is actually a B
  }    
}

甚至更糟:

vector<C*> filterForC's(A* parent) {
  vector<Element*> eleVec = parent.getChildren();
  vector<C*> retVec;
  for(Element* e : eleVec) {
    if (e.getType() == "class C") {
      C *c = dynamic_cast<C*>(e);
      retVec.append(c);
    }
  }
}

它绝对是面向对象的。它确实使用继承。但感觉就像我只是把 OOP 给我的所有舒适感都扔到了一边,决定再次使用原始指针和位移。谷歌搜索这个主题,我发现很多人说上下选角是糟糕的设计或糟糕的做法。我完全相信这是真的,但我想知道为什么。我不能更改大部分代码,因为它是一个更大项目的一部分,但我想知道如何在将来设计程序时应对这种情况。

我的问题:

  1. 为什么上下铸造被认为是糟糕的设计,除了它看起来很糟糕的事实?
  2. dynamic_cast慢吗?
  3. 有什么经验法则可以避免像我上面解释的那样的设计吗?

关于 SO 这里有很多关于dynamic_cast的问题。我只读了几本,也不经常在我自己的代码中使用这种方法,所以我的回答反映了我对这个主题的看法,而不是我的经验。小心。

(1.)为什么上下铸造被认为是糟糕的设计,除了它看起来很糟糕的事实?

(3.)是否有任何经验法则可以避免像我上面解释的设计?

在阅读Stroustrup C++常见问题解答时,我有一个中心信息:不要相信那些说永远不要使用某种工具的人。相反,使用正确的工具来完成手头的任务。

然而,有时,两种不同的工具可能具有非常相似的目的,这里也是如此。您基本上可以使用dynamic_castvirtual函数重新编码任何功能。

那么dynamic_cast什么时候是正确的工具呢?(另请参阅dynamic_cast的正确用例是什么?

  • 一种可能的情况是,当您有一个无法扩展的基类,但仍需要编写类似重载的代码时。使用动态投射,您可以进行非侵入性操作。

  • 另一个是你想要保留一个接口,即一个纯虚拟基类,并且不想在任何派生类中实现相应的虚函数。

然而,通常情况下,你宁愿依赖虚拟功能——即使只是为了减少丑陋。此外,它更安全:动态转换可能会失败并终止您的程序,而虚拟函数调用(通常)不会。

此外,在纯函数方面实现,当您添加新的派生类时,您不会忘记在所有必需的地方更新它。另一方面,动态转换很容易在代码中被遗忘。

示例的虚拟函数版本

这里又是一个例子:

Element* currentElement;
void addToCurrentElement(const C *c) {
  if(A *a = dynamic_cast<A*>(currentElement)) {
    //doSomething, if not, check if currentElement is actually a B
  }    
}

要重写它,请在您的基中添加一个(可能是纯的)虚函数add(A*)add(B*)add(C*),您在派生类中重载了这些函数。

struct A : public Element
{
     virtual add(A* c) { /* do something for A */ }
     virtual add(B* c) { /* do something for B */ }
     virtual add(C* c) { /* do something for C */ }
};
//same for B, C, ...

然后在函数中调用它,或者可能编写更简洁的函数模板

template<typename T>
void addToCurrentElement(T const* t)
{
    currentElement->add(t);
}

我会说这是标准方法。如前所述,缺点可能是对于纯虚函数,您需要N*N重载,而N可能就足够了(例如,如果只有A::add需要特殊处理)。

其他替代方法可能使用 RTTI、CRTP 模式、类型擦除等。


(2.)dynamic_cast慢吗?

当考虑整个网络状态中的大多数答案时,是的,动态转换似乎很慢,例如请参阅此处。然而,我没有实际经验来支持或反驳这种说法。