跨二进制模块实现单例

时间:2015-10-01 09:20:40

标签: c++ c++11 singleton dynamic-linking boost-log

背景

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

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

.dll

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

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

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

get()

非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; } 指定在此模块中实现(定义)实体(即函数,类或对象)并且是其他模块引用。另一方面,__declspec(dllexport)指定实体未在此模块中实现,并且可以在其他模块中找到。

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

__declspec(dllimport)

作为旁注,此处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>(); 说明符不是强制性的。见this S.O.问题

问题

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

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

最后,如果您了解制作单身的更好解决方案,请随时将其作为答案发布。

1 个答案:

答案 0 :(得分:2)

  

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

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

与dll一起出现了一个关联的lib文件,其中包含用于将应用程序与dll链接的辅助数据。将应用程序与库链接时,链接器使用lib文件解析对dll符号的引用,并在应用程序二进制文件中组成导入表。当应用程序启动时,操作系统(或操作系统的运行时加载程序组件)使用导入表来查找应用程序所依赖的dll以及从这些dll导入的符号。然后,它使用dll中的导出表来解析dll中引用符号的地址,并完成链接过程。

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

在Linux和其他类似POSIX的操作系统上,情况有很大不同。链接时,对于每个共享对象(可以是so库或应用程序可执行文件),将编译符号表。它列出了此共享对象实现的符号以及它缺少的符号。另外,链接器可以在共享对象中嵌入可以用于解析缺失符号的其他共享对象(可选地,具有搜索路径)的列表。运行时加载程序包括一个链接器,它按顺序加载共享对象,并构造一个包含所有共享对象的符号的全局符号表。在构造该表时,来自多个共享对象的重复符号被解析为单个实现(因为所有实现被认为是等效的,所以使用加载列表中实现该符号的第一个共享对象)。当链接顺序中的后续共享对象被加载时,任何缺失的符号也会被解析。

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

现在,有一些方法可以影响链接过程,特别是有一些方法可以限制从共享对象导出的符号。最常见的方法是使用symbol visibilitylinker scripts。使用这些工具,可以实现非常接近Windows的链接行为及其所有优点和缺点。请注意,限制符号可见性时,必须使用visibility attributepragma标记要从共享对象导出的符号。但是,没有必要标记要导入的符号。

  

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

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

  

最后,如果您了解制作单身的更好解决方案,请随时将其作为答案发布。

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