C++标准实践:虚接口类 vs. 模板

50

我需要就泛化与多态作出一个决定。

情景如下:我想让我的单olithic互相依赖的代码更加模块化,干净和可扩展。 目前这个阶段改变设计原则是可行的,而且在我看来也非常值得。

我是否应该引入纯虚基类(接口)或者模板呢?

我了解模板选项的基础知识:少一些间接性,性能更好,编译速度更快; 但是没有后期绑定等等。

STL并没有太多(或者根本没有)使用继承,Boost也是如此。但是我认为它们旨在成为程序员每两行代码就使用一次的非常基本的工具。

我认为继承和晚期绑定的方法更适用于插件式的大型代码片段和功能,这些功能应该在部署之后甚至在运行时进行交换、更新等。

我的情景有点介于两者之间。

我不需要在运行时动态交换代码,编译时可以。通常它也是一个非常核心和频繁使用的功能,在逻辑上不能分成大块。

这让我倾向于使用模板解决方案。对我来说,它看起来也更加清晰。

是否存在任何重大的不良影响?接口是否仍然是最佳选择?它们什么时候不是?哪一个更符合标准的C++风格?

我知道这有点主观,但我真的很想听听经验。我没有Scott Meyers的effective C++,所以我寄希望于你们:)


与模板和虚拟类的速度/性能相关的问题:https://dev59.com/s2ox5IYBdhLWcg3w3X9S - Trevor Boyd Smith
6个回答

33

你说的基本正确,动态多态性(继承、虚函数)通常是当类型需要在运行时改变时的最佳选择(例如在插件架构中)。静态多态性(模板)是如果类型只应在编译时更改,则更好的选择。

模板的唯一潜在缺点是它们通常必须在头文件中定义(这意味着要包含更多代码),这经常导致编译时间较慢。

但从设计上来看,我不能看出在可能使用模板时存在任何问题。

哪种更符合标准 C++ 风格?

这取决于“标准C++风格”是什么。C ++标准库使用了各种方法。STL 对一切都使用模板,稍早的 IOStreams 库使用继承和虚函数,并且从 C 继承的库函数两者都不使用,当然。

如今,模板是迄今为止最受欢迎的选择,并且我必须说这是最“标准”的方法。


3
我看到使用模板而不是接口有一个问题:需求是完全隐含的。当你必须实现纯虚函数时,你会得到它的确切签名。但当你看到一个模板类型,比如_AllocT或Iter,你不知道你的类需要什么,也不知道它是否必须是一个类。你唯一能知道的是通过查找一个良好的文档,但今天我尝试创建自己的STL兼容分配器类时遇到了麻烦。 - Virus721
7
你唯一能够知道的方法就是寻找一份良好的说明文档,或者尝试编译并查看编译器无法找到哪些函数。此外,概念旨在解决这个问题。(即使它是一个接口,你仍然需要找到良好的文档。仅仅知道要覆盖哪些函数是不够的。你还需要知道它们的语义是什么,而接口并不能告诉你这一点)。尽管如此,你说得对。语言支持两种方式也有其原因 :) - jalf
如果C++支持,用接口替换多个模板是否更加简洁? - ar2015

13

经典面向对象多态的特点:

  • 对象在运行时绑定;这更加灵活,但也会在运行时消耗更多资源(CPU)
  • 强类型带来了更多类型安全性,但需要使用dynamic_cast(并且它可能会导致客户端的问题),这可能很容易抵消其优点
  • 可能更广为人知和理解,但是“传统”的深度继承层次结构对我来说似乎很可怕

通过模板的编译时多态的特点:

  • 编译时绑定可以进行更积极的优化,但阻止了运行时的灵活性
  • 鸭子类型可能看起来更奇怪,但失败通常是编译时的失败
  • 有时可能更难阅读和理解;没有概念,编译器诊断有时可能变得令人恼火

请注意,无需决定使用哪种方式。您可以自由地混合使用它们两个(以及许多其他惯用语和范例)。通常,这会导致非常出色(而且表达力强)的代码。 (例如,请参见类型擦除之类的内容。)如果想了解通过巧妙混合范例可以实现什么,您可能想要浏览 Alexandrescu 的“现代 C++设计”。


面向对象编程中的强类型?这甚至没有意义。您可以从编译时多态性中获得所有类型安全性的好处。在面向对象编程中,类型擦除(例如通过在接口后隐藏实际类型)和上下转换几乎消除了您可能对类型安全性的任何希望。 - jalf
3
你说得没错。不过,我的意思是,在运行时多态性中,你可以确定所获得的是某个接口的有意实现,而在编译时鸭子类型的多态性可能会接受任何偶然匹配。 - sbi

9

这是一种虚假的对立。是的,继承和虚函数的主要用途在于 iostreams,它们非常古老,并且以与 std 库的其余部分相当不同的风格编写。

但是,许多“最酷”的现代 C++ 库(例如 boost)确实利用了运行时多态性,只是使用模板使其更方便使用。

boost::anystd::tr1::function(以前也来自 boost)是很好的例子。

它们都是单个项目的容器,其中具体类型在编译时未知(这在 any 中特别明显,因为它有自己的动态转换操作符以获取值)。


+1 你的回答非常有启发性,但为了遵守问题,我接受了jalf的建议。谢谢。 - AndreasT

6
在我积累了一些经验后,有一些关于模板的东西我不喜欢: 模板元编程存在某些缺点,使其不能成为可用的语言:
  • 可读性:太多的括号,太多的非语言强制(因此被误用)的约定
  • 对于正在进行常规编程语言演变的人来说,模板是难以阅读和理解的(只需看看boost bgl即可)
  • 有时感觉像是有人试图在awk中编写c++代码生成器。
  • 编译器错误消息是杂乱无章的废话
  • 需要太多的黑客技巧(其中大部分在c++0x中得到纠正)才能获得一些基本的“语言”功能。
  • 没有模板实现文件,导致头文件库(这是一个非常两面的剑)
  • 通常的IDE代码完成功能在模板方面帮助不大。
  • 在MPL中做大事似乎很麻烦,找不到其他词来形容。每一行模板化代码都会对该模板类型产生约束,并以一种文本替换的方式强制执行。继承层次结构中固有的语义,模板结构中则完全没有。就像一切都是void*,编译器试图告诉你是否会出现段错误。

话虽如此,我在基本实用程序和库上使用它非常成功。但对于编写高级功能或硬件相关的东西,似乎不太适合我。 这意味着我将我的构建块模板化,但以传统方式构建房屋。


1

我在我的大型代码库中同时使用两者。当类型在编译时已知时,我使用模板进行设计;当其只在运行时已知时,则使用虚函数。我发现虚函数更易于编写和阅读,但是在性能关键时刻,如果使用模板多态(如果您真的可以称之为多态),那么可以内联它的事实确实有所帮助。


0

就我个人而言,最好的技术取决于你擅长什么。如果您对面向对象编程(OO)有更多经验,请使用OO。如果您对泛型有更多经验,请使用泛型。

两种技术都有一些相当的模式,这意味着对于很多事情,您可以使用任何一种。例如, OO中的策略与泛型中的策略或OO中的模板方法与泛型中的常见重复模式。

如果您计划重构已经正常运行但结构有点混乱的生产代码,请不要将其作为玩弄某些新设计技术的借口,因为一两年后,一旦您更好地理解了该技术,您可能会后悔如何重构您的代码。在学习技术时,很容易引入新的不灵活性。目标是改善现有代码的设计。如果您不熟练掌握一项技术,那么您怎么知道您正在改善设计,而不是在代码中建立大型象征性符号。

就我个人而言,我更擅长OO,并倾向于使用它,因为我知道我可以制定清晰易懂且大多数人可以更改的干净设计。我编写的大多数通用代码都旨在与其他通用代码进行接口交互,例如编写迭代器或用于算法的通用函数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接