Lin*_*gxi 5 c++ singleton dynamic-linking boost-log c++11
首先,我认为这个问题超出了C++标准。该标准处理多个翻译单元(实例化单元),从而处理多个目标模块,但似乎没有承认具有多个独立编译和链接的二进制模块(即.soLinux 上的文件和.dllWindows 上的文件)的可能性。毕竟,后者或多或少地进入了应用程序二进制接口(ABI)的世界,标准目前将其留给实现来考虑。
当仅涉及单个二进制模块时,以下代码片段说明了一个优雅且可移植(符合标准)的单例解决方案。
inline T& get() {
static T var{};
return var;
}
Run Code Online (Sandbox Code Playgroud)
此解决方案有两点需要注意。首先,inline说明符使该函数成为包含在多个翻译单元中的候选函数,这非常方便。请注意,该标准保证最终二进制模块中只有一个实例get()和局部静态变量(请参见此处)。var
第二件需要注意的事情是,从 C++11 开始,静态局部变量的初始化是正确同步的(请参阅此处的静态局部变量部分)。因此,并发调用就可以了。get()
现在,我尝试将此解决方案扩展到涉及多个二进制模块的情况。我发现以下变体适用于 Windows 上的 VC++。
// 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;
}
Run Code Online (Sandbox Code Playgroud)
非Windows用户注意: __declspec(dllexport)指定一个实体(即函数、类或对象)在此模块中实现(定义)并被其他模块引用。__declspec(dllimport)另一方面,指定实体未在此模块中实现,而是在其他模块中找到。
由于 VC++ 支持导出和导入模板实例化(请参阅此处),因此上述解决方案甚至可以模板化。例如:
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>();
Run Code Online (Sandbox Code Playgroud)
附带说明一下,inline这里的说明符不是强制性的。请参阅这个问题。
由于 GCC 和 clang 中没有__declspec(dllexport/import)等效项,有没有办法制作适用于这两个编译器的上述解决方案的变体?
另外,在 Boost.Log 中,我注意到了该BOOST_LOG_INLINE_GLOBAL_LOGGER_DEFAULT宏(请参阅此处的全局记录器对象部分)。即使应用程序由多个模块组成,它也声称可以创建单例。如果有人知道这个宏的内部工作原理,欢迎在这里解释。
最后,如果您知道任何更好的单身人士解决方案,请随时将其作为答案发布。
由于 GCC 和 clang 中没有
__declspec(dllexport/import)等效项,有没有办法制作适用于这两个编译器的上述解决方案的变体?
首先,这并不是一个与编译器相关的问题,而是一个底层操作系统问题。GCC(以及据说是 clang)__declspec(dllexport/import)在 Windows 上提供支持,并且基本上与 MSVC 对以此方式标记的函数和对象所做的操作相同。基本上,标记的符号被放置在从 dll 导出的符号表(导出表)中。例如,当您在运行时查询 dll 中的符号时,可以使用该表(请参阅GetProcAddress)。
与 dll 一起出现的是一个关联的 lib 文件,其中包含用于将应用程序与 dll 链接的辅助数据。当您将应用程序与库链接时,链接器使用 lib 文件来解析对 dll 符号的引用,并在应用程序二进制文件中组成导入表。当应用程序启动时,操作系统(或者更确切地说是操作系统的运行时加载器组件)使用导入表来查找应用程序依赖的 dll 以及从这些 dll 导入的符号。然后,它使用 dll 中的导出表来解析 dll 中引用的符号的地址并完成链接过程。
此过程的重要副作用是,仅动态解析导入的符号,并且动态链接到的每个符号都与特定的 dll 关联。您可以在多个 dll 和应用程序本身中使用同名的符号,只要不导出这些符号,它们就会引用不同的实体。如果导出它们,链接过程将由于不明确而失败。这使得进程范围的单例在 Windows 上变得困难。这也打破了一些 C/C++ 语言规则,因为通过外部链接(在语言术语中)获取对象或函数的地址可能会在程序的不同部分产生不同的地址。另一方面,DLL 更加独立,并且在较小程度上依赖于加载上下文。
Linux 和其他类似 POSIX 的操作系统上的情况有很大不同。链接时,将为每个共享对象(可以是 so 库或应用程序可执行文件)编译一个符号表。它列出了该共享库实现的符号和它缺少的符号。此外,链接器可以将其他共享对象的列表(可选地,带有搜索路径)嵌入到共享对象中,这些共享对象可用于解析丢失的符号。运行时加载器包括链接器,该链接器顺序加载共享对象并构建包括来自所有共享对象的符号的全局符号表。构造该表时,来自多个共享对象的重复符号将解析为单个实现(因为所有实现都被认为是等效的,所以使用加载列表中实现该符号的第一个共享对象)。当加载链接顺序中的后续共享对象时,也会解决任何丢失的符号。
此过程的效果是,具有外部链接的每个符号解析为共享对象之一中的单个实现,即使多个共享对象实现它也是如此。这更符合C/C++语言规则,并且更容易实现进程范围的单例。一个简单的函数局部静态变量(不以任何特殊方式标记)就足够了。
现在,有多种方法可以影响链接过程,特别是有多种方法可以限制从共享对象导出的符号。最常见的方法是使用符号可见性和链接描述文件。使用这些工具,可以实现非常接近 Windows 的链接行为,并具有其所有优点和缺点。请注意,当您限制符号可见性时,您必须使用属性visibility 或编译指示标记要从共享对象导出的符号。不过,无需标记要导入的符号。
另外,在 Boost.Log 中,我注意到了该
BOOST_LOG_INLINE_GLOBAL_LOGGER_DEFAULT宏(请参阅此处的全局记录器对象部分)。即使应用程序由多个模块组成,它也声称可以创建单例。如果有人知道这个宏的内部工作原理,欢迎在这里解释。
当从多模块应用程序使用 Boost.Log 时,需要将其构建为共享库。这使得它可以在整个应用程序中声明全局记录器的进程范围引用存储(该存储在 Boost.Log dll/so 中实现)。当您获得使用BOOST_LOG_INLINE_GLOBAL_LOGGER_DEFAULT或类似宏声明的记录器时,首先会在存储中查找对记录器的引用。如果未找到,则创建记录器并将对其的引用存储回内部存储器。否则,将使用现有参考。与引用缓存一起,这提供了非常接近函数局部静态变量的性能。
最后,如果您知道任何更好的单身人士解决方案,请随时将其作为答案发布。
尽管这并不是一个真正的答案,但您通常应该避免使用单例。它们很难以不影响性能的方式正确实施。如果您确实必须实现一个,那么类似于 Boost.Log 的解决方案看起来足够通用。但请注意,使用此解决方案通常不知道哪个模块创建了(因此“拥有”)单例,因此您无法动态卸载任何模块。可能有更简单的具体情况的方法,例如导出返回对本地静态对象的引用的函数。如果您想要可移植性并默认支持非默认符号可见性,请始终显式导出符号。
| 归档时间: |
|
| 查看次数: |
1455 次 |
| 最近记录: |