【问题标题】:Implementing singletons across binary modules跨二进制模块实现单例
【发布时间】:2015-10-01 09:20:39
【问题描述】:

背景

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

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

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

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

要注意的第二件事是,从 C++11 开始,静态局部变量的初始化已正确同步(请参阅 静态局部变量 部分 here)。所以,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;
}

非 Windows 用户注意事项: __declspec(dllexport) 指定实体(即,函数、类或对象)在此模块中实现(定义)并由其他模块。另一方面,__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 说明符在这里不是强制性的。见this S.O.问题。

问题

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

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

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

【问题讨论】:

  • 您能否为非 Windows 用户解释 dllimport/dllexport 的确切作用以及使用多极 .so 文件时为什么会出现问题?理想情况下,给出一个简单的测试代码。
  • @Lingxi 好吧,为了设计,我会问自己是否需要单例来实现你想要的。
  • @πάνταῥεῖ 事实上,我只是想要一种方便的方法来创建和共享多个二进制模块的实例,就像 BOOST_LOG_INLINE_GLOBAL_LOGGER_DEFAULT 所做的那样。
  • @灵曦嗯。 dllimport 与简单的 extern 有何不同?
  • @Walter 也许,它会阻止模块从函数体中自己生成定义,而是在另一个模块中查找定义。

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


【解决方案1】:

由于 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 的解决方案看起来足够通用。但是请注意,使用此解决方案通常不知道哪个模块创建了(因此,“拥有”)单例,因此您无法动态卸载任何模块。可能有更简单的特定于案例的方法,例如导出返回对本地静态对象的引用的函数。如果您希望可移植性并默认支持非默认符号可见性,请始终明确导出您的符号。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-10-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-01-07
    • 1970-01-01
    相关资源
    最近更新 更多