Boost 实现变体
接口与实现分离
boost.org 库组件(以及一般高质量软件)的接口规范在概念上与这些接口的实现是分离的。这可能并不明显,特别是当一个组件完全在头文件中实现时,但接口和实现的这种分离始终是被假定的。从那些关注软件设计、可移植性和标准化的人的角度来看,接口才是重要的,而实现只是一个细节。
Dietmar Kühl,boost.org 的最初贡献者之一,评论说:“主要贡献是接口,它通过一个实现来增强,证明了实现相应类是可能的,并提供了一个免费的实现。”
实现变体
可能需要接口的多种实现,以适应平台依赖性或性能权衡。平台依赖性的示例包括编译器缺陷、文件系统、线程机制和图形用户界面。性能权衡的经典示例是使用大量内存的快速实现与使用较少内存的较慢实现。
Boost 库通常使用 配置头文件 boost/config.hpp 来捕获编译器和平台依赖性。虽然不强制使用 boost/config.hpp,但它是简单配置问题的首选方法。
Boost 策略
Boost 的策略是避免接口规范中出现平台相关的变体,但提供可在各种平台和应用程序上使用的实现。这意味着 Boost 库将使用下面描述的技术来处理平台依赖性。
Boost 关于旨在提高性能的实现变体的策略是避免它们,除非收益大大超过全部成本。“全部成本”一词旨在包括有形成本(如额外的维护)和无形成本(如增加用户理解的难度)。
提供实现变体的技术
可以使用几种技术来提供实现变体。每种技术在某些情况下适用,而在其他情况下不适用。
单一通用实现
第一种技术是根本不提供实现变体。而是提供单一的通用实现,并放弃所有其他技术所暗示的复杂性。
适用情况: 当可以编写一个在各种平台上都具有合理性能的单一可移植实现时。当替代实现仅在深奥的方式上有所不同时,尤其适用。
不适用情况: 当实现需要平台特定功能,或者当存在性能特征差异很大的多种可能实现时。
Beman Dawes 评论说:“在设计讨论中,有时声称某种实现比另一种实现快得多,但计时测试发现没有显着差异。教训是,虽然算法差异可能会显着影响速度,但编码差异(例如将类从虚成员更改为非虚成员或删除一级间接寻址)不太可能产生任何可衡量的差异,除非在内部循环深处。即使在内部循环中,现代 CPU 通常也会在相同的时钟周期数内执行此类竞争代码序列!单一的通用实现通常就足够了。”
正如 Donald Knuth 所说,“过早优化是万恶之源。”(《计算概览》,第 6 卷,第 4 期,第 268 页)。
宏
虽然宏的弊端是众所周知的,但在少数情况下,宏仍然是首选解决方案
- 通过 #include 保护防止头文件的多次包含。
- 将次要配置信息从配置头文件传递到其他文件。
适用情况: 对于小的编译时变体,否则安装、使用或维护的成本会很高或令人困惑。更适合在库组件内部和之间进行通信,而不是与库用户进行通信。
不适用情况: 如果其他技术可以做到。
为了最大限度地减少宏的负面影响
- 仅当宏明显优于其他技术时才使用宏。它们应被视为最后的手段。
- 名称应全部大写并以命名空间名称开头。这将最大限度地减少名称冲突的机会。例如,名为 foobar.h 的 Boost 头文件的 #include 保护可以命名为 BOOST_FOOBAR_H。
单独的文件
一个库组件可以有多个变体,每个变体都包含在自己的单独文件或文件中。最合适的变体的文件在安装时被复制到相应的 include 或 implementation 目录。
在 Boost 库中提供此方法的方式是将专门的实现作为单独的文件包含在 .ZIP 发行文件中的单独子目录中。例如,名为 foobar 的库的 .ZIP 发行文件中的结构,该库同时具有默认变体和专门变体,可能看起来像
foobar.h // The default header file foobar.cpp // The default implementation file readme.txt // Readme explains when to use which files self_contained/foobar.h // A variation with everything in the header linux/foobar.cpp // Implementation file to replace the default win32/foobar.h // Header file to replace the default win32/foobar.cpp // Implementation file to replace the default
适用情况: 当不同的平台需要不同的实现,或者当可能的实现之间存在重大性能差异时。
不适用情况: 当在同一安装中使用多个变体中的一个以上变体有意义时。
单独的组件
与其拥有单个组件的多个实现变体,不如提供几个单独的组件。例如,Boost 库目前提供 scoped_ptr
和 shared_ptr
类,而不是单个 smart_ptr
类,该类被参数化以区分这两种情况。有几种方法可以选择组件
- 由程序员在编码期间硬编码。
- 由程序员编写的运行时逻辑选择(以牺牲一些额外的空间、时间以及程序复杂性为代价,来换取在运行时选择实现的能力。)
适用情况: 当变体的接口不同时,以及当使用多个变体中的一个以上变体是合理的时候。当需要运行时选择实现时。
不适用情况: 当变体是数据类型、特性或专门化变体,可以通过使组件成为模板来更好地处理时。当变体的选择最好通过程序本身之外的一些设置或安装机制来完成时,也不适用。因此,通常不适合应对平台差异。
注意: 还有一种相关技术,其中接口被指定为抽象(纯虚)基类(或接口定义语言),并且实现选择被传递给第三方,例如动态链接库或对象请求代理。虽然这是一种强大的技术,但它远远超出了本文的讨论范围。
基于模板的方法
将类或函数转换为模板通常是应对变体的优雅方法。基于模板的方法提供最佳的空间和时间效率,以换取将实现选择限制为编译时。
重要的模板技术包括
- 数据类型参数化。这允许单个组件对各种数据类型进行操作,这也是最初发明模板的原因。
- 特性参数化。如果参数化很复杂,将各个方面捆绑到一个特性辅助类中可以实现很大的变化,同时隐藏混乱的细节。C++ 标准库提供了此习惯用法的几个示例,例如
iterator_traits<>
(24.3.1 lib.iterator.traits) 和char_traits<>(21.2 lib.char.traits)。 - 专门化。模板参数可以完全用于选择专门化。例如
SomeClass<fast> my_fast_object; // fast and small are empty classes SomeClass<small> my_small_object; // used just to select specialization
适用情况: 当需要变体是由于数据类型或特性或与性能相关(如在几种算法之间进行选择)时,以及当程序可能合理地使用多个变体中的一个以上变体时。
不适用情况: 当变体的接口不同,或者当变体的选择最好通过程序本身之外的某种机制来完成时。因此,通常不适合应对平台差异。