在C ++中编译时间与运行时多态性的优点/缺点

时间:2013-06-01 18:36:28

标签: c++ architecture polymorphism

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

我认为编译时代的多态性编译代码会更大(为模板类型创建更多的方法/类定义),编译时间会给你更大的灵活性,而运行时会让你更安全"更安全#34;多态性(即偶然难以错误地使用)。

我的假设是否正确?是否还有其他优点/缺点?任何人都可以给出一个具体的例子,其中两个都是可行的选择,但其中一个或另一个是明显更好的选择吗?

此外,编译时多态是否会产生更快的代码,因为没有必要通过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();
}

3 个答案:

答案 0 :(得分:6)

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

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

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

答案 1 :(得分:2)

在你过滤掉明显不好和次优的案件后,我相信你几乎什么都没有。当你面临这样的选择时,IMO很少见。你可以通过陈述一个例子来改进这个问题,并为此提供真正的比较。

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

虚函数与函数指针一样,使您无法创建准确的调用图。您可以从顶部查看底部但不容易。虚函数应遵循一些规则,但如果不遵守,则必须为罪人查看所有这些规则。

同样在性能方面也有一些损失,在大多数情况下可能不是很大,但如果在另一方没有平衡,为什么要接受?

答案 2 :(得分:1)

  

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

     

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

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

  • 使用模板,只实际调用的函数被实例化
  • 死码消除
  • 常量数组维度允许成员变量(如T mydata[12];)与对象一起分配,自动存储局部变量等,而运行时多态实现可能需要使用动态分配(即new[]) - 在某些情况下,这会显着影响缓存效率
  • 内联函数调用,这使得小对象获取/设置操作等微不足道的事情在我实施基准测试的实现上快了一个数量级
  • 避免虚拟调度,这相当于跟随指向函数指针表的指针,然后对其中一个进行外线调用(它通常是影响性能的外线方面大多数)
  

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

模板当然可以:

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

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

  • 由于上面提到的仅用于实例化功能的方面,你可以“走开”#34;实例化一个类模板,其类型只有一些类模板的函数可以编译:在某些方面很糟糕,因为程序员可能会期望他们看似有效的Template<MyType>将支持所有Template<>支持其他类型的操作,只是在尝试特定操作时失败;在其他方面它很好,因为如果你对所有操作不感兴趣,你仍然可以使用Template<>

    • 如果Concepts [Lite]成为未来的C ++标准,程序员可以选择在语义操作上加强前期约束,那些类型用作模板参数必须支持,这将是当用户发现他们的Template<MyType>::operationX被破坏时,避免令人讨厌的惊喜,并且通常在编译的早期提供更简单的错误消息
  

...虽然运行时会让你更安全&#34;多态性(即偶然难以错误地使用)。

可以说,鉴于上面的模板灵活性,它们更加严格。主要&#34;安全&#34;运行时多态性的问题是:

  • 一些问题最终会鼓励&#34;胖&#34;接口(在Stroustrup中提到的C ++编程语言):具有仅适用于某些派生类型的函数的API,算法代码需要保持&#34;询问&#34;派生类型&#34;我应该为你做这个&#34; &#34;你能做到这一点&#34; &#34;做了那项工作&#34; 等..

  • 你需要虚拟析构函数:有些类没有它们(例如std::vector) - 使它更难以安全地从它们派生,并且虚拟调度表的对象指针不是&# 39; t跨进程有效,使得很难将运行时多态对象放在共享内存中以供多个进程访问

  

任何人都可以给出一个具体的例子,其中两者都是可行的选择,但其中一个或另一个明显是更好的选择吗?

不确定。假设您正在编写一个快速排序函数:您只能支持从具有虚拟比较函数和虚拟交换函数的某些Sortable基类派生的数据类型,或者您可以编写使用{{1策略参数默认为Lessstd::less<T>。鉴于某种类型的性能绝大部分取决于这些比较和交换操作的性能,因此模板更适合于此。这就是为什么C ++ std::swap<>明显优于C库的通用std::sort函数,它使用函数指针来实现虚拟调度的C实现。有关详情,请参阅here

  

此外,编译时多态是否会产生更快的代码,因为没有必要通过vtable调用函数,或者这是否会被编译器优化掉?

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