【问题标题】:Dyamic vs Static Polymorphism in C++ : which is preferable?C ++中的动态与静态多态性:哪个更可取?
【发布时间】:2012-03-28 11:57:31
【问题描述】:

我了解动态/静态多态性取决于应用程序设计和要求。但是,如果可能的话,是否总是选择静态多态性而不是动态多态性?特别是,我可以在我的应用程序中看到以下两种设计选择,这两种选择似乎都被建议不要使用:

  1. 使用 CRTP 实现静态多态性:没有 vtable 查找开销,同时仍以模板基类的形式提供接口。但是,使用大量的 switch 和 static_cast 来访问正确的类/方法,这是危险的

  2. 动态多态性:实现接口(纯虚拟类),为访问器/突变器等微不足道的函数关联查找成本

我的应用程序对时间非常关键,所以我赞成静态多态性。但是需要知道使用过多的 static_cast 是否表明设计不佳,以及如何在不产生延迟的情况下避免这种情况。

编辑:感谢您的洞察力。举一个具体的案例,哪种方法更好?

class IMessage_Type_1
{
  virtual long getQuantity() =0;
...
}
class Message_Type_1_Impl: public IMessage_Type_1
{
  long getQuantity() { return _qty;}
...
}

template <class T>
class TMessage_Type_1
{
  long getQuantity() { return static_cast<T*>(this)->getQuantity(); }
...
}
class Message_Type_1_Impl: public TMessage_Type_1<Message_Type_1_Impl>
{
  long getQuantity() { return _qty; }
...
}

请注意,每个类中有几个修改器/访问器,我确实需要在我的应用程序中指定一个接口。在静态多态中,我只切换一次——获取消息类型。但是,在动态多态性中,我使用虚函数进行每个方法调用。这不是使用静态多边形的理由吗?我相信 CRTP 中的 static_cast 非常安全并且没有性能损失(编译时间限制)?

【问题讨论】:

  • switch-case 结构与使用 vtable 具有相同的复杂性。
  • 我不知道为什么你认为在使用静态多态性时你会得到很多 static_cast 和开关 - 你能展示一些示例代码吗?
  • @MichaelAnderson:如果你在需要动态多态的情况下尝试使用静态多态,你就会明白这一点。在这种情况下,使用动态多态几乎肯定比尝试重新发明它更好。
  • @MikeSeymour 同意。但即使在这些情况下,您通常也可以使用类似于访问者模式的方式将任何此类强制转换/切换到一个或两个函数。
  • 另一种使用静态多态性(不可扩展)的方式:Boost.Variant。同样,稍微不同的问题需要稍微不同的工具。

标签: c++ optimization polymorphism static-polymorphism


【解决方案1】:

静态和动态多态性旨在解决不同的 问题,因此很少有两种情况都适用。在 在这种情况下,动态多态性将导致更灵活和 更易于管理设计。但大多数时候,选择将是 很明显,出于其他原因。

两者的粗略分类:虚函数允许不同 通用接口的实现;模板允许不同的 通用实现的接口。

【讨论】:

  • virtual functions allow different implementations for a common interface; templates allow different interfaces for a common implementation. 这真的没有意义。静态多态性实际上是允许两个不同的类创建自己的实现,但方法名称相同,并且在两种情况下都从基类调用。
  • @Fellowshee 在最常见的用途中,模板类为每种实现类型提供不同的接口,但相同的实现:例如std::vector&lt;double&gt; 提供push_back(double) 函数,而std::vector&lt;int&gt; 提供push_back(int)。不同的接口;相同的实现。从虚拟push_back(double) 继承将允许不同的实现,但不允许不同的接口。
  • @JamesKanze 模板化设计的另一个非常常见的用途是,通过使用部分特化,实现相反的效果,根据传递类型的属性提供不同的优化实现。
【解决方案2】:

切换只不过是一系列跳转,经过优化后,跳转到通过表查找的地址。和虚函数调用一模一样。

如果必须根据类型进行跳转,则必须先选择类型。如果无法在编译时进行选择(主要是因为它取决于输入),则必须始终执行两个操作:选择和跳转。您用于选择的句法工具不会改变性能,因为优化相同。

事实上,您正在重新发明 v-table。

【讨论】:

  • 我同意 switch case 基本上是一个 vtable。然而,在动态多态中,我们对每个方法都使用虚拟,而在静态中,我们需要使用一次切换和强制转换来获得派生类型,之后就没有间接了。在问题中添加了一个示例。有没有更好的方法来实现这一点?
  • @vid: 如果允许调用者知道派生类型,那么多态的目的到底是什么?在这种情况下,调用者可以直接与派生类交互,没有任何抽象、接口等。似乎多态和继承在这里都是多余的。如果不允许调用者知道派生类型,则应在基类中的每个调用上执行类型转换和方法分派。 vtable 是这种调度的最佳解决方案。 CRPT 是优化从基类到派生类的调用的正确工具(仅限)
  • 好的,我很欣赏你的观点。我试图做的是使用 CRTP 指定一个接口。这样,当其他开发者想要扩展库时,他们再次使用CRTP继承基类,如果他们没有实现任何方法,就会抛出编译错误。与纯虚拟基类相同的逻辑,但没有 vtable 开销。有没有一些我能想到的成语更合适地实现这一点?
  • @vid:实际上我们可以讨论两个接口:(i)库和外部调用者之间的接口(客户端接口)(ii)核心库类与其后代之间的接口(扩展接口)。我建议您考虑将这两个界面设计为两个单独的(!)任务
  • @vid: ... CRPT 可能真的“指定”唯一的前(扩展)接口(ii)客户端接口的设计(i)取决于向客户端公开派生类的可能性。如果这样的阐述是不可能的(不希望的),那么使用虚函数,没有办法做得更好。
【解决方案3】:

您会看到与纯粹基于模板的多态性相关的设计问题。虽然看起来虚拟基类可以让您很好地了解派生类的期望,但在大量模板化的设计中,这变得更加困难。可以通过在使用其中一个 boost 库时引入语法错误来轻松证明这一点。

另一方面,您担心使用虚函数时会出现性能问题。证明这将是一个问题要困难得多。

恕我直言,这不是问题。除非另有说明,否则请坚持使用虚拟功能。虚函数调用比大多数人想象的要快得多(从动态链接库调用函数也增加了一层间接性。似乎没有人想到这一点)。

我只会考虑模板化设计,如果它使代码更易于阅读(通用算法),您使用已知的虚拟函数(数字算法)很慢的少数情况之一,或者您已经将其确定为性能瓶颈.

【讨论】:

  • 模板还强制(在编译时)类型系统上的谓词。如果我使用std::vector&lt;A&gt;,我知道没有人会在不经意间将B 放入其中。插入的类型将始终对应于我期望提取的类型。
  • 谢谢@ebo。我确实将虚函数作为我的应用程序中的瓶颈,编辑了问题以使开销更清晰
  • 我确实看到了您的担忧。仍然:您测量过,它真的是一个问题吗?
【解决方案4】:

如果调用的方法可以由编译器内联,则静态多态可能会提供显着的优势。 例如,如果虚拟方法如下所示:

protected:
virtual bool is_my_class_fast_enough() override {return true;}

那么静态 polimophism 应该是首选方式(否则,该方法应该是诚实的并返回 false :)。

“True”虚拟调用(在大多数情况下)不能内联。

其他差异(例如 vtable 调用中的额外间接)可以忽略

[编辑]

但是,如果您真的需要运行时多态性 (如果调用者不应该知道该方法的实现,因此该方法不能在调用者一侧内联)然后 不要重新发明 vtable(正如 Emilio Garavaglia 提到的),只需使用它。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-22
    • 2013-07-20
    相关资源
    最近更新 更多