Boost C++ 库

...世界上最受推崇和专业设计的 C++ 库项目之一。 Herb SutterAndrei Alexandrescu,《C++ 编码标准

包含独立源文件的 Boost 库作者指南

这些指南是为需要单独编译源代码才能使用的 Boost 库的作者设计的。在本文档中,本指南引用了一个虚构的 “whatever” 库,因此在复制示例时,请将所有出现的 “whatever” 或 “WHATEVER” 替换为您自己的库名称。

目录

影响源代码的更改
防止编译器 ABI 冲突
静态库或动态库
支持 Windows Dll
使用 auto_link.hpp 自动选择和链接库
影响构建系统的更改
创建库 Jamfile
测试自动链接
版权

影响源代码的更改

防止编译器 ABI 冲突

有些编译器(主要是 Microsoft Windows 编译器!)具有一系列编译器开关,这些开关会改变 C++ 类和函数的 ABI。例如,考虑 Borland 的编译器,它具有以下选项

-b    (on or off - effects enum sizes).
-Vx   (on or off - empty members).
-Ve   (on or off - empty base classes).
-aX   (alignment - 5 options).
-pX   (Calling convention - 4 options).
-VmX  (member pointer size and layout - 5 options).
-VC   (on or off, changes name mangling).
-Vl   (on or off, changes struct layout). 

除了影响使用哪个运行时库的选项之外,还提供了这些选项(稍后会详细介绍);选项的总组合数可以通过将上面的各个选项相乘获得,因此总共得到 2*2*2*5*4*5*2*2 = 3200 种组合!

问题在于,用户通常希望能够构建 Boost 库,然后只需链接到它们,一切就能正常工作,而不管他们的项目设置如何。无论这是否是一个合理的期望,如果没有一些管理此问题的方法,用户很可能会发现他们的程序会遇到奇怪且难以追踪的运行时崩溃,除非他们链接到的库是使用与其项目相同的选项构建的(默认对齐设置的更改是主要原因)。管理此问题的一种方法是使用“前缀和后缀”标头:这些标头调用编译器特定的 #pragma 指令,以指示编译器,无论后面的代码是用特定的编译器 ABI 设置构建(或将要构建)的。

Boost.config 提供了宏 BOOST_HAS_ABI_HEADERS,只要有可用于正在使用的编译器的前缀和后缀标头,就会设置该宏。在类似这样的标头中的典型用法如下

#ifndef BOOST_WHATEVER_HPP
#define BOOST_WHATEVER_HPP

#include <boost/config.hpp>

// this must occur after all of the includes and before any code appears:
#ifdef BOOST_HAS_ABI_HEADERS
#  include BOOST_ABI_PREFIX
#endif
//
// this header declares one class, and one function by way of examples:
//
class whatever
{
   // details.
};

whatever get_whatever();

// the suffix header occurs after all of our code:
#ifdef BOOST_HAS_ABI_HEADERS
#  include BOOST_ABI_SUFFIX
#endif

#endif

如果您愿意,也可以在库源文件中包含此代码,尽管您可能不需要这样做

  • 如果您在库源文件中使用这些(但在库的标头中使用),并且用户尝试使用非默认 ABI 设置编译库源,那么如果存在任何冲突,他们将收到编译器错误。
  • 如果您确实在库的标头和库源文件中都包含它们,那么无论使用什么编译器设置,代码都应该始终编译,尽管结果可能与用户期望的不符:因为我们已将 ABI 强制恢复为默认模式。

理由

如果没有一些管理此问题的方法,用户经常会报告类似“当我想调用它时,你的傻库总是崩溃”之类的错误。这些问题可能非常困难且耗时,最终才发现是编译器设置更改了程序的类和/或函数类型的 ABI,与预编译库中的 ABI 不同。使用前缀/后缀标头可以最大限度地减少此问题,但可能无法完全消除它。

反驳论点 #1

信任用户,如果他们想要 13 字节对齐(!),就让他们拥有它。

反驳论点 #2

前缀/后缀标头有“蔓延”到其他 Boost 库的趋势 - 例如,如果 boost::shared_ptr<> 构成您的类的 ABI 的一部分,那么在您的代码中包含前缀/后缀标头将毫无用处,除非 shared_ptr.hpp 也使用它们。仅标头的 Boost 库的作者可能不太热衷于此解决方案 - 这有一定的道理 - 因为他们没有面临相同的问题。

静态库或动态库

当用户的运行时是动态链接的时,Boost 库可以构建为动态库(Unix 平台上的 .so,Windows 上的 .dll)或静态库(Unix 平台上的 .a,Windows 上的 .lib)。因此,我们可以选择默认支持哪种库

  • 在 Unix 平台上,这对代码通常没有影响:用户只需在其 makefile 中选择他们更喜欢链接到的库即可。
  • 在 Windows 平台上,代码必须经过特殊注释才能支持 DLL,因此我们需要选择一个选项作为默认选项,另一个作为备用选项。
  • 在 Windows 平台上,我们可以注入特殊代码来自动选择要链接的库变体:因此,我们再次需要决定哪个是默认值(请参阅下面的自动链接部分)。

建议默认选择静态链接。

理由

这里没有一种适合所有情况的策略。

当前行为的理由是从 Boost.Regex(及其祖先 regex++)继承而来的:此库最初在运行时为动态时默认使用动态链接。例如,如果您从 dll 中使用 regex,那么实际上更安全。但是,此行为带来了一连串用户的抱怨:主要是关于部署,都询问是否可以将静态链接作为默认设置。在 regex 更改行为后,投诉停止了,作者也没有收到过关于默认静态链接是错误选择的投诉。

请注意,其他库可能需要做出其他选择:例如,旨在用于实现 dll 插件的库几乎在所有情况下都需要使用动态链接。

支持 Windows Dll

在大多数类 Unix 平台上,为了将源代码编译为共享库,不需要对源代码进行特殊注释,因为所有外部符号都已公开。但是,大多数 Windows 编译器都要求要从 dll 导入或导出的符号,必须以 __declspec(dllimport) 或 __declspec(dllexport) 为前缀。如果没有对源代码进行这种修改,就不可能在 Windows 上正确构建共享库(历史注释 - 最初在 16 位 Windows 上需要这些声明修饰符,因为导出的类的内存布局与“本地”类的内存布局不同 - 尽管这不再是一个问题,但仍然没有办法指示链接器“导出所有内容”,还有待观察 64 位 Windows 是否会重新启用分段架构,从而首先导致此问题。另请注意,导出的符号的修饰名称与未导出的符号的修饰名称不同,因此需要 __declspec(dllimport) 才能链接到 dll 中的代码)。

为了支持在 MS Windows 上构建共享库,您的代码必须将库导出的所有符号都加上一个宏前缀(我们将其称为 BOOST_WHATEVER_DECL),您的库将定义该宏以扩展为 __declspec(dllexport) 或 __declspec(dllimport) 或不扩展为任何内容,具体取决于您的库的构建或使用方式。典型用法如下所示

#ifndef BOOST_WHATEVER_HPP
#define BOOST_WHATEVER_HPP

#include <boost/config.hpp>

#ifdef BOOST_HAS_DECLSPEC // defined in config system
// we need to import/export our code only if the user has specifically
// asked for it by defining either BOOST_ALL_DYN_LINK if they want all boost
// libraries to be dynamically linked, or BOOST_WHATEVER_DYN_LINK
// if they want just this one to be dynamically liked:
#if defined(BOOST_ALL_DYN_LINK) || defined(BOOST_WHATEVER_DYN_LINK)
// export if this is our own source, otherwise import:
#ifdef BOOST_WHATEVER_SOURCE
# define BOOST_WHATEVER_DECL __declspec(dllexport)
#else
# define BOOST_WHATEVER_DECL __declspec(dllimport)
#endif  // BOOST_WHATEVER_SOURCE
#endif  // DYN_LINK
#endif  // BOOST_HAS_DECLSPEC
//
// if BOOST_WHATEVER_DECL isn't defined yet define it now:
#ifndef BOOST_WHATEVER_DECL
#define BOOST_WHATEVER_DECL
#endif

//
// this header declares one class, and one function by way of examples:
//
class BOOST_WHATEVER_DECL whatever
{
   // details.
};

BOOST_WHATEVER_DECL whatever get_whatever();

#endif
然后在该库的源代码中,人们会使用
 
// 
// define BOOST_WHATEVER SOURCE so that our library's 
// setup code knows that we are building the library (possibly exporting code), 
// rather than using it (possibly importing code): 
// 
#define BOOST_WHATEVER_SOURCE 
#include <boost/whatever.hpp> 

// class members don't need any further annotation: 
whatever::whatever() { } 
// but functions do: 
BOOST_WHATEVER_DECL whatever get_whatever() 
{
   return whatever();
}

导入/导出依赖项

除了导出您的主要类和函数(那些实际记录在案的类和函数)之外,如果您尝试导入/导出其依赖项也未导出的类,Microsoft Visual C++ 将会大声且频繁地发出警告。依赖项包括:任何基类、用作数据成员的任何用户定义的类型,以及您的依赖项的所有依赖项等等。当依赖项是模板类时,这会引起特殊问题,因为尽管从技术上讲可以导出这些类,但并非易事,尤其是当模板本身具有特定于实现的详细信息的依赖项时。在大多数情况下,最好只使用以下命令抑制警告

#ifdef BOOST_MSVC
#  pragma warning(push)
#  pragma warning(disable : 4251 4231 4660)
#endif

// code here

#ifdef BOOST_MSVC
#pragma warning(pop)
#endif

只要没有依赖项是具有非恒定静态数据成员的(模板)类,这都是安全的,这些类确实需要导出,否则程序中将存在静态数据成员的多个副本,这真的非常糟糕。

历史注释:在 16 位 Windows 上,您确实必须导出所有依赖项,否则代码将无法工作,但是由于最新的 Visual Studio .NET 支持导入/导出单个成员函数,因此可以合理地肯定 Windows 编译器在导入/导出类时不会做任何恶意的事情 - 例如更改类的 ABI。

理由

为什么要费心 - 导入/导出机制难道不会占用比类本身更多的代码吗?

这是一个很好的观点,而且可能是真的,但是在某些情况下,库代码必须放在共享库中 - 例如,当应用程序由多个 dll 以及可执行文件组成,并且多个 dll 链接到同一个 Boost 库时 - 在这种情况下,如果库不是动态链接的,并且它包含任何全局数据(即使该数据是库内部的私有数据),那么可能会发生非常糟糕的事情 - 即使没有全局数据,我们仍然会得到代码膨胀效应。顺便说一句,对于较大的应用程序,将应用程序拆分为多个 dll 可能非常有利 - 通过使用 Microsoft 的“延迟加载”功能,应用程序将仅加载在任何给定时间真正需要的部分,从而给人留下应用程序响应更快、加载速度更快的印象。

为什么默认使用静态链接?

在上面的工作示例中,代码假定库将静态链接,除非用户另有要求。大多数用户似乎更喜欢这种方式(没有单独的 dll 要分发,而且整体分发大小通常也小得多:即,您只需为您使用的内容付费,不多也不少),但这是一种主观的判断,有些库甚至可能只有动态版本(例如 Boost.threads)。

使用 auto_link.hpp 自动选择和链接库

许多 Windows 编译器都附带多个运行时库 - 例如,Microsoft Visual Studio .NET 附带 6 个版本的 C 和 C++ 运行时。至关重要的是,用户链接到的 Boost 库是针对与其程序构建时相同的 C 运行时构建的。如果不是这种情况,那么用户最多会遇到链接器错误,最坏的情况是运行时崩溃。Boost 构建系统通过提供不同的构建变体来管理这一点,每个变体都是针对不同的运行时构建的,并且根据针对哪个运行时构建,会获得略有不同的修饰名称。例如,当使用 Visual Studio .NET 2003 构建时,regex 库的命名如下

boost_regex-vc71-mt-1_31.lib
boost_regex-vc71-mt-gd-1_31.lib
libboost_regex-vc71-mt-1_31.lib
libboost_regex-vc71-mt-gd-1_31.lib
libboost_regex-vc71-mt-s-1_31.lib
libboost_regex-vc71-mt-sgd-1_31.lib
libboost_regex-vc71-s-1_31.lib
libboost_regex-vc71-sgd-1_31.lib

现在的困难在于选择用户应该链接到其中的哪一个。

相比之下,大多数 Unix 编译器通常只有一个运行时(或者有时如果有单独的线程安全选项,则为两个)。对于这些系统,选择正确库变体的唯一选择是他们是否需要调试信息,以及可能需要的线程安全性。

从历史上看,Microsoft Windows 编译器通过提供 #pragma 选项来管理此问题,该选项允许库的标头自动选择要链接到的库。这使得一切都变得自动化,并且对于最终用户来说非常容易:一旦他们包含一个具有单独源代码的头文件,正确的库构建变体的名称就会嵌入到目标文件中,并且只要该库在链接器搜索路径中,它就会被链接器拉入,而无需任何用户干预。

通过包含标头 <boost/config/auto_link.hpp>,可以在 Boost 库中启用自动库选择和链接,首先定义 BOOST_LIB_NAME,如果适用,则定义 BOOST_DYN_LINK。

//
// Automatically link to the correct build variant where possible. 
// 
#if !defined(BOOST_ALL_NO_LIB) && !defined(BOOST_WHATEVER_NO_LIB) && !defined(BOOST_WHATEVER_SOURCE)
//
// Set the name of our library, this will get undef'ed by auto_link.hpp
// once it's done with it:
//
#define BOOST_LIB_NAME boost_whatever
//
// If we're importing code from a dll, then tell auto_link.hpp about it:
//
#if defined(BOOST_ALL_DYN_LINK) || defined(BOOST_WHATEVER_DYN_LINK)
#  define BOOST_DYN_LINK
#endif
//
// And include the header that does the work:
//
#include <boost/config/auto_link.hpp>
#endif  // auto-linking disabled

库的用户文档应注意,可以通过定义 BOOST_ALL_NO_LIB 或 BOOST_WHATEVER_NO_LIB 来禁用该功能

如果出于任何原因您需要调试此功能,如果您首先定义 BOOST_LIB_DIAGNOSTIC,则标头 <boost/config/auto_link.hpp> 将输出一些有用的诊断消息。

影响构建系统的更改

创建库 Jamfile

用于构建库 “whatever” 的 Jamfile 通常位于 boost-root/libs/whatever/build 中,唯一需要的额外步骤是在库目标中添加 <define> 要求,以便您的代码知道它正在构建 dll 还是静态库,典型的 Jamfile 如下所示

lib boost_regex : ../src/whatever.cpp : 
  <link>shared:<define>BOOST_WHATEVER_DYN_LINK=1 ;
 

测试自动链接

测试自动链接功能有些复杂,并且需要访问支持该功能的编译器:有关示例,请参阅 libs/config/test/link/test/Jamfile.v2