Boost 实现变体
接口与实现的分离
boost.org 库组件(以及一般高质量软件)的接口规范在概念上与这些接口的实现是分开的。这可能并不明显,尤其是在组件完全在头文件中实现时,但这种接口与实现的分离总是被假设的。从关注软件设计、可移植性和标准化的人员的角度来看,接口才是重要的,而实现只是一个细节。
Boost.org 的最初贡献者之一 Dietmar Kühl 评论道:“主要的贡献是接口,它与一个实现一起增强,证明了实现相应的类是可能的,并提供了一个免费的实现。”
实现变体
可能需要接口的多个实现,以适应平台依赖性或性能权衡。平台依赖性的示例包括编译器缺陷、文件系统、线程机制和图形用户界面。性能权衡的经典示例是使用大量内存的快速实现与使用较少内存的较慢实现。
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。
单独的文件
库组件可以有多个变体,每个变体都包含在自己的单独文件或文件中。安装时,将最合适变体的文件复制到相应的包含或实现目录。
在 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
适用情况:当需要变体是由于数据类型或特性,或者与性能相关(例如在几种算法之间进行选择)时,以及当程序可能合理地使用多个变体时。
不适用情况:当变体的接口不同时,或者当变体的选择最好由程序本身之外的某些机制完成时。因此,通常不适合处理平台差异。