跨二进制模块实现单例

Implementing singletons across binary modules

背景

首先,我认为这个问题超出了C++标准。该标准涉及多个翻译单元(实例化单元),因此涉及多个目标模块,但似乎不承认具有多个独立编译和链接的二进制模块(即 Linux 上的 .so 文件和Windows 上的 .dll 个文件)。毕竟,后者或多或少进入了标准目前留给实施考虑的 application binary interface (ABI) 世界。

当只涉及一个二进制模块时,以下代码片段展示了一个优雅且可移植(符合标准)的单例解决方案。

inline T& get() {
  static T var{};
  return var;
}

这个解决方案有两点需要注意。首先,inline 说明符使函数成为包含在多个翻译单元中的候选者,这非常方便。请注意,标准保证在最终二进制模块中只有一个 get() 实例和局部静态变量 var(参见 here)。

第二点要注意的是,自 C++11 起,静态局部变量的初始化已正确同步(参见 静态局部变量 部分 here)。因此,并发调用 get() 没问题。

现在,我尝试将此解决方案扩展到涉及多个二进制模块的情况。我发现以下变体适用于 VC++ on Windows.

// dllexport is used in building the library module, and
// dllimport is used in using the library in an application module.
// Usually controlled by a macro switch.
__declspec(dllexport/dllimport) inline T& get() {
  static T var{};
  return var;
}

非Windows 用户注意事项: __declspec(dllexport) 指定实体(即函数、class 或对象) 在此模块中实现(定义)并被其他模块引用。另一方面,__declspec(dllimport) 指定实体未在此模块中实现,而是在其他模块中找到。

由于VC++支持导出和导入模板实例化(参见here),上述解决方案甚至可以被模板化。例如:

template <typename T> inline
T& get() {
  static T var{};
  return var;
}

// EXTERN is defined to be empty in building the library module, and 
// to `extern` in using the library module in an application module.
// Again, this is usually controlled by a macro switch.
EXTERN template __declspec(dllexport/dllimport) int& get<int>();

作为旁注,inline 说明符在这里不是强制性的。参见 S.O。问题。

问题

由于在 GCC 和 clang 中没有 __declspec(dllexport/import) 等价物,有没有办法制作适用于这两个编译器的上述解决方案的变体?

此外,在 Boost.Log 中,我注意到 BOOST_LOG_INLINE_GLOBAL_LOGGER_DEFAULT 宏(参见 全局记录器对象 部分 here)。据称即使应用程序包含多个模块,也可以创建单例。如果有人知道这个宏的内部工作原理,欢迎在这里解释。

最后,如果您知道制作单例的任何更好的解决方案,请随时post作为答案。

Since there is no __declspec(dllexport/import) equivalents in GCC and clang, is there a way to make a variant of the above solution that works on these two compilers?

首先,这与其说是一个与编译器相关的问题,不如说是一个底层操作系统问题。 GCC(据说还有 clang)在 Windows 上支持 __declspec(dllexport/import),并且基本上与 MSVC 对以这种方式标记的函数和对象所做的相同。基本上,标记的符号放置在从 dll 导出的符号的 table 中(导出 table)。例如,当您在 运行 时间内查询 dll 中的符号时(请参阅 GetProcAddress),可以使用此 table。

与 dll 一起出现的是一个关联的 lib 文件,其中包含用于 link 使用 dll 调用您的应用程序的辅助数据。当您 link 您的应用程序使用该库时,link 开发者使用 lib 文件解析对 dll 符号的引用,并在您的应用程序二进制文件中编写导入 table。当应用程序启动时,OS(或者更确切地说 OS 的 运行time loader 组件)使用导入 table 来找出您的应用程序依赖的 dll 和什么它从那些 dll 导入的符号。然后使用dll中的export tables解析dll中引用符号的地址,完成linking过程。

此过程的重要副作用是只有导入的符号会被动态解析,并且您动态 link 到的每个符号都与特定的 dll 相关联。您可以在多个 dll 和应用程序本身中使用同名符号,只要不导出这些符号,它们就会引用不同的实体。如果它们被导出,linking 过程将因歧义而失败。这使得进程范围的单例在 Windows 上变得困难。这也打破了一些 C/C++ 语言规则,因为获取具有外部 linkage 的对象或函数的地址(在语言术语中)可以在程序的不同部分产生不同的地址。另一方面,dll 更加独立,并且在较小程度上依赖于加载上下文。

Linux 和其他 POSIX-like OS 上的情况明显不同。当 linked 时,对于每个共享对象(可以是 so 库或应用程序 executable)编译一个 table 符号。它列出了这个共享库实现的符号和它缺少的符号。此外,linker 可以在共享对象中嵌入其他共享对象的列表(可选,带有搜索路径),可用于解析丢失的符号。 运行 时间加载器包括一个 link 程序,它按顺序加载共享对象并构建全局 table 符号,其中包含来自所有共享对象的符号。在构造 table 时,来自多个共享对象的重复符号被解析为单个实现(因为所有实现都被认为是等效的,所以使用加载列表中实现该符号的第一个共享对象)。加载 link 顺序中的后续共享对象时,也会解决任何丢失的符号。

此过程的效果是每个具有外部 linkage 的符号解析为一个共享对象中的单个实现,即使多个共享对象实现它也是如此。这更符合 C/C++ 语言规则,并且可以更简单地实现进程范围的单例。一个简单的函数局部静态变量,没有以任何特殊方式标记,就足够了。

现在,有一些方法可以影响 linking 过程,特别是有一些方法可以限制从共享对象导出的符号。最常见的方法是使用 symbol visibility and linker scripts. With these tools it is possible to achieve linking behavior very close to Windows, with all its pros and cons. Note that when you limit symbol visibility you do have to mark the symbols you intend to export from the shared object with the visibility attribute or pragma。不过不需要标记要导入的符号。

Also, in Boost.Log, I noticed the BOOST_LOG_INLINE_GLOBAL_LOGGER_DEFAULT macro (see the Global logger objects section here). It is claimed to create singletons even if the application consists of multiple modules. If someone knows about the inner workings of this macro, explanations are welcome here.

Boost.Log 需要在多模块应用程序中使用时构建为共享库。这使得它可以在整个应用程序中声明对全局记录器的引用的进程范围存储(存储在 Boost.Log dll/so 中实现)。当您获得使用 BOOST_LOG_INLINE_GLOBAL_LOGGER_DEFAULT 或类似宏声明的记录器时,首先会查找存储以查找对记录器的引用。如果未找到,则创建记录器并将对它的引用存储回内部存储器。否则使用现有参考。与引用缓存一起,这提供了非常接近函数局部静态变量的性能。

Finally, if you know about any better solutions for making singletons, feel free to post it as an answer.

虽然这不是真正的答案,但您通常应该避免使用单例。它们很难以不影响性能的方式正确实施。如果您确实必须实施一个,那么类似于 Boost.Log 的解决方案看起来就足够通用了。但是请注意,使用此解决方案通常不知道哪个模块创建了(因此,'owns')单例,因此您不能动态卸载任何模块。可能有更简单的特定情况的方法,例如导出返回对本地静态对象的引用的函数。如果您想要可移植性并默认支持非默认符号可见性,请始终明确导出您的符号。