在 C++ 中,当可以使用运行时(子类、虚函数)或编译时(模板、函数重载)多态性来实现相同的功能时,为什么要选择其中一个呢?
我认为编译后的代码对于编译时多态性会更大(为模板类型创建更多方法/类定义)......
通常是的 - 由于模板参数的不同组合的多个实例化,但请考虑:
- 使用模板,只有实际调用的函数被实例化
- 死代码消除
- 常量数组维度允许成员变量,例如
T mydata[12];
与对象一起分配,自动存储局部变量等,而运行时多态实现可能需要使用动态分配(即new[]
) - 这在某些情况下会显着影响缓存效率
- 函数调用的内联,这使得小对象获取/设置操作等琐碎的事情在我基准测试的实现上快了一个数量级
- 避免虚拟调度,这相当于跟踪指向函数指针表的指针,然后对其中一个进行离线调用(通常是离线方面最损害性能)
...而且编译时间会给你更多的灵活性......
模板当然可以:
给定为不同类型实例化的相同模板,相同的代码可能意味着不同的东西:例如,T::f(1)
可能在一个实例中调用一个void f(int) noexcept
函数,在另一个实例中调用一个仿函数对象virtual void f(double)
在另一个实例中;从另一个角度来看,不同的参数类型可以以最适合它们的方式提供模板化代码所需的内容T::f
operator()(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 调用函数,或者编译器是否会优化它?
它通常更快,但偶尔模板代码膨胀的总和影响可能会压倒编译时多态性通常更快的无数方式,因此总的来说它更糟。