【问题标题】:Compile time vs run time polymorphism in C++ advantages/disadvantagesC++ 中的编译时与运行时多态性的优点/缺点
【发布时间】:2013-06-01 18:36:27
【问题描述】:

在 C++ 中,当可以使用运行时(子类、虚函数)或编译时(模板、函数重载)多态性来实现相同的功能时,为什么要选择其中一个?

我认为对于编译时多态性(为模板类型创建更多方法/类定义),编译后的代码会更大,并且编译时会给你更多的灵活性,而运行时会给你“更安全”的多态性(即更难意外误用)。

我的假设是否正确?两者还有其他优点/缺点吗?谁能举一个具体的例子,两者都是可行的选择,但其中一个显然是更好的选择?

另外,编译时多态性是否会产生更快的代码,因为不需要通过 vtable 调用函数,或者编译器是否已经优化了这一点?

例子:

class Base
{
  virtual void print() = 0;
}

class Derived1 : Base
{
  virtual void print()
  {
     //do something different
  }
}

class Derived2 : Base
{
  virtual void print()
  {
    //do something different
  }
}

//Run time
void print(Base o)
{
  o.print();
}

//Compile time
template<typename T>
print(T o)
{
  o.print();
}

【问题讨论】:

  • 编译时多态性的主要缺点是有时你无法做到这一点。就像您通过界面管理对象一样。只要您可以使用编译时,猜测它在大多数情况下会更好。只需一点点努力,您就可以让您的日常工作同时接受这两者。
  • “运行时”示例无法编译 - 可以传递基类指针并实现动态调度,但由于 Base 是抽象的,因此无法创建对象。

标签: c++ architecture polymorphism


【解决方案1】:

静态多态性产生更快的代码,主要是因为积极内联的可能性。虚函数很少可以内联,并且主要是在“非多态”场景中。在C++ FAQ 中查看此项目。如果速度是你的目标,你基本上别无选择。

另一方面,当使用静态多态时,不仅编译时间,而且代码的可读性和可调试性也差很多。例如:抽象方法是强制执行某些接口方法的一种干净方式。要使用静态多态实现相同的目标,您需要恢复到概念检查或curiously recurring template pattern

真正必须使用动态多态的唯一情况是在编译时实现不可用;例如,当它从动态库加载时。但在实践中,您可能希望以性能换取更简洁的代码和更快的编译。

【讨论】:

  • 拼命寻找这个答案:使用 CRTP 时,如何遍历从同一个基类继承的不同类,并调用覆盖基类的类函数?编译器在编译时确实知道类列表。那么为什么调度到运行时:/
【解决方案2】:

在您过滤掉明显不好和不理想的情况后,我相信您几乎一无所有。当您面临这种选择时,IMO 非常罕见。您可以通过举例来改进问题,并为此提供真实的比较车。

假设我们有这个现实的选择,我会选择编译时解决方案——为什么要浪费运行时来做一些不是绝对必要的事情?还有一些事情是在编译时决定的,它更容易思考、跟随头脑并进行评估。

虚拟函数,就像函数指针一样,使您无法创建准确的调用图。您可以查看底部但不容易从顶部查看。虚函数应遵循一些规则,但如果不遵循,则必须将它们全部查找为罪人。

还有一些性能损失,在大多数情况下可能不是什么大问题,但如果另一方面没有平衡,为什么要这样做?

【讨论】:

  • 我明白你的意思,很难想出一个例子。
【解决方案3】:

在 C++ 中,当可以使用运行时(子类、虚函数)或编译时(模板、函数重载)多态性来实现相同的功能时,为什么要选择其中一个?

我认为编译后的代码对于编译时多态性会更大(为模板类型创建更多方法/类定义)...

经常是的 - 由于模板参数的不同组合的多个实例化,但请考虑:

  • 使用模板,只实例化实际调用的函数
  • 死代码消除
  • 恒定的数组维度允许成员变量(例如T mydata[12];)与对象一起分配,自动存储局部变量等,而运行时多态实现可能需要使用动态分配(即new[]) - 这可以显着在某些情况下会影响缓存效率
  • 内联函数调用,这使得小对象 get/set 操作等琐碎的事情在我已进行基准测试的实现中速度提高了一个数量级
  • 避免虚拟调度,这相当于跟随一个指向函数指针表的指针,然后对其中一个进行离线调用(通常是离线方面最损害性能)李>

...编译时间会给你更多的灵活性...

模板当然可以:

  • 给定为不同类型实例化的相同模板,相同的代码可能意味着不同的东西:例如,T::f(1) 可能在一个实例中调用 void f(int) noexcept 函数,在另一个实例中调用 virtual void f(double)T::f 函子对象的operator()(float) 在另一个;从另一个角度来看,不同的参数类型可以以最适合它们的方式提供模板化代码所需的内容

  • SFINAE 让您的代码在编译时调整以使用对象支持的最有效接口,而无需对象主动提出建议

  • 由于上面提到的仅调用实例化函数的方面,您可以“摆脱”使用只有类模板的某些函数可以编译的类型来实例化类模板:在某些方面这很糟糕,因为程序员可能期望他们看似工作的Template&lt;MyType&gt; 将支持Template&lt;&gt; 支持的其他类型的所有操作,只是在他们尝试特定操作时它会失败;在其他方面很好,因为如果您对所有操作不感兴趣,您仍然可以使用Template&lt;&gt;

    • 如果 Concepts [Lite] 成为未来的 C++ 标准,程序员将可以选择对用作模板参数的类型必须支持的语义操作施加更强大的预先约束,这将当用户发现他们的Template&lt;MyType&gt;::operationX 损坏时,避免令人讨厌的意外,并且通常在编译的早期给出更简单的错误消息

...虽然运行时会给你“更安全”的多态性(即更难被意外错误地使用)。

可以说,考虑到上述模板的灵活性,它们更加严格。运行时多态性的主要“安全”问题是:

  • 一些问题最终会鼓励“胖”接口(在 C++ 编程语言中 Stroustrup 提到的意义上):API 的函数仅适用于某些派生类型,算法代码需要不断“询问”派生类型“我应该为你做这件事吗?”“你能做这个吗”“那行得通吗”等等。

  • 您需要虚拟析构函数:某些类没有它们(例如std::vector) - 使得从它们安全派生变得更加困难,并且指向虚拟调度表的对象内指针在进程间无效,这使得它很难将运行时多态对象放在共享内存中以供多个进程访问

谁能举一个具体的例子,两者都是可行的选择,但其中一个显然是更好的选择?

当然。假设您正在编写一个快速排序函数:您只能支持从具有虚拟比较函数和虚拟交换函数的某个 Sortable 基类派生的数据类型,或者您可以编写一个使用Less 策略参数的排序模板默认为std::less&lt;T&gt;std::swap&lt;&gt;。鉴于排序的性能主要由这些比较和交换操作的性能支配,因此模板更适合于此。这就是为什么 C++ std::sort 明显优于 C 库的通用 qsort 函数,后者使用函数指针来有效地实现虚拟调度的 C 实现。有关详细信息,请参阅here

另外,编译时多态性是否会产生更快的代码,因为不需要通过 vtable 调用函数,或者编译器是否已经优化了这一点?

它通常更快,但偶尔模板代码膨胀的总和影响可能会压倒编译时多态性通常更快的无数方式,因此总的来说它更糟。

【讨论】:

    猜你喜欢
    • 2011-01-10
    • 2017-10-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多