6

在 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();
}
4

3 回答 3

7

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

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

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

于 2013-06-01T19:43:45.087 回答
2

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

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

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

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

于 2013-06-01T19:32:25.283 回答
2

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

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

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

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

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

模板当然可以:

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

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

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

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

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

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

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

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

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

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

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

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

于 2016-01-15T11:24:02.400 回答