Boost C++ 库

……世界上最受推崇和设计精良的 C++ 库项目之一。 Herb SutterAndrei AlexandrescuC++ 编码规范

包含独立源代码的 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 库,然后只需链接到它们,一切就能正常工作,无论他们的项目设置是什么。无论这是否是一个合理的期望,如果没有一些方法来管理这个问题,用户可能会发现,除非他们链接到的库是用与他们的项目相同的选项构建的(对默认对齐设置的更改是罪魁祸首),否则他们的程序可能会在运行时遇到奇怪且难以追踪的崩溃。

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 使用正则表达式,那么这样实际上更安全。但是,这种行为带来了持续不断的用户投诉:主要关于部署,所有用户都询问静态链接是否可以作为默认值。在正则表达式更改行为后,投诉停止了,作者没有收到过关于静态链接作为默认值是错误选择的投诉。

请注意,其他库可能需要做出其他选择:例如,旨在用于实现 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 构建时,正则表达式库的命名如下:

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 获取示例。