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

Compile time vs run time polymorphism in C++ advantages/disadvantages

本文关键字:c++ 优缺点 多态性 运行时 编译      更新时间:2023-10-16

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

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

我的假设正确吗?这两种方法还有其他优点/缺点吗?谁能给出一个具体的例子,说明两者都是可行的选择,但其中一个显然是更好的选择?

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

的例子:

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

静态多态产生更快的代码,主要是因为有可能使用激进的内联。虚拟函数很少可以内联,主要是在"非多态"场景中。参见c++ FAQ中的这一项。如果速度是你的目标,你基本上没有选择。

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

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

过滤掉明显不好和次优的情况后,我相信您几乎什么都没有。在我看来,当你面临这种选择时,这种情况是非常罕见的。你可以通过举一个例子来改进这个问题,并为此提供一个真正的比较。

假设我们有现实的选择,我会去编译时的解决方案——为什么浪费运行时的东西不是绝对必要的?也就是在编译时决定的东西,它更容易思考,跟随头部并进行计算。

虚函数,就像函数指针一样,使您无法创建准确的调用图。你可以从下到上复习,但不容易从上到下复习。虚函数应该遵循一些规则,但如果没有,你就必须从所有的规则中寻找罪人。

也有一些性能损失,在大多数情况下可能不是什么大问题,但如果没有平衡,为什么要接受它?

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

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

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

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

…这样的编译时间会给你更多的灵活性…

模板当然可以:

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

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

  • 由于上面提到的instantiate-only-functions-called方面,您可以"get away";用一个只有部分类模板的函数可以编译的类型实例化一个类模板:在某些方面,这是不好的,因为程序员可能期望他们的Template<MyType>表面上工作将支持Template<>支持其他类型的所有操作,只有在他们尝试特定操作时才会失败;在其他方面,它是好的,因为如果你对所有的操作不感兴趣,你仍然可以使用Template<>

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

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

有争议的是,鉴于上面的模板灵活性,它们更严格。主要的"安全"运行时多态性的问题有:

  • 一些问题最终助长了"肥胖";接口(在Stroustrup在《c++编程语言》中提到的意义上):带有只对某些派生类型起作用的函数的api,算法代码需要保持"询问"。派生类型"我应该为你做这个吗">"你能做这个">"那工作了吗">等等。

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

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

确定。假设您正在编写一个快速排序函数:您只能支持从某个带有虚拟比较函数和虚拟交换函数的Sortable基类派生的数据类型,或者您可以编写一个使用默认为std::less<T>std::swap<>Less策略参数的排序模板。如果排序的性能完全由这些比较和交换操作的性能决定,那么模板更适合于此。这就是为什么c++std::sort明显优于C库的通用qsort函数,后者使用函数指针来有效地实现C中的虚拟分派。

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

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