从 DLL 导出包含 `std::` 对象(矢量、地图等)的类
Posted
技术标签:
【中文标题】从 DLL 导出包含 `std::` 对象(矢量、地图等)的类【英文标题】:Exporting classes containing `std::` objects (vector, map etc.) from a DLL 【发布时间】:2009-04-20 09:38:26 【问题描述】:我正在尝试从包含 std::vectors
和 std::strings
等对象的 DLL 导出类 - 整个类通过以下方式声明为 DLL 导出:
class DLL_EXPORT FontManager
问题在于,对于复杂类型的成员,我会收到以下警告:
warning C4251: 'FontManager::m__fonts' : class 'std::map<_Kty,_Ty>' needs to have dll-interface to be used by clients of class 'FontManager' with [ _Kty=std::string, _Ty=tFontInfoRef ]
即使我没有更改成员变量本身的类型,我也可以通过在它们之前放置以下前向类声明来删除一些警告:
template class DLL_EXPORT std::allocator<tCharGlyphproviderRef>;
template class DLL_EXPORT std::vector<tCharGlyphProviderRef,std::allocator<tCharGlyphProviderRef> >;
std::vector<tCharGlyphProviderRef> m_glyphProviders;
看起来前向声明“注入”DLL_EXPORT
用于编译成员时,但它安全吗?
当客户端编译此标头并在他身边使用std::
容器时,它真的会改变什么吗?
它会在未来使用这种容器DLL_EXPORT
(并且可能不是内联)吗?
它真的解决了警告试图警告的问题吗?
这个警告是我应该担心的,还是最好在这些构造的范围内禁用它? 客户端和 DLL 将始终使用相同的库和编译器集构建,并且这些都是仅标头类...
我正在使用带有标准 STD 库的 Visual Studio 2003。
更新
我想更多地针对您,因为我看到答案是一般性的,这里我们谈论的是标准容器和类型(例如 std::string
) - 也许问题真的是:
我们能否通过相同的库头禁用客户端和 DLL 都可用的标准容器和类型的警告,并像对待 int
或任何其他内置类型一样对待它们? (它似乎在我这边工作正常)
如果是这样,我们可以这样做的条件应该是什么?
或者是否应该禁止使用此类容器,或者至少要格外小心以确保没有赋值运算符、复制构造函数等会内联到 DLL 客户端中?
一般来说,我想知道您是否觉得设计一个具有此类对象的 DLL 接口(例如,使用它们将内容作为返回值类型返回给客户端)是一个好主意,为什么,我会喜欢对此功能有一个“高级”接口... 也许最好的解决方案是 Neil Butterworth 建议的 - 创建一个静态库?
【问题讨论】:
推荐相关阅读:***.com/q/5347355/103167 【参考方案1】:当您从客户端接触班级中的成员时,您需要提供一个 DLL 接口。 DLL 接口意味着编译器在 DLL 本身中创建函数并使其可导入。
因为编译器不知道 DLL_EXPORTED 类的客户端使用了哪些方法,所以它必须强制所有方法都是 dll 导出的。 它必须强制客户端可以访问的所有成员也必须 dll 导出其功能。当编译器警告您方法未导出并且客户端的链接器发送错误时,就会发生这种情况。
并非每个成员都必须用 dll-export 标记,例如客户无法触及的私人成员。在这里您可以忽略/禁用警告(注意编译器生成的 dtor/ctors)。
否则成员必须导出他们的方法。 使用 DLL_EXPORT 前向声明它们不会导出这些类的方法。您必须在其编译单元中将相应的类标记为 DLL_EXPORT。
归结为...(对于不可导出 dll 的成员)
如果您有客户不能/不能使用的成员,请关闭警告。
如果您有客户端必须使用的成员,请创建 dll 导出包装器或创建间接方法。
要减少外部可见成员的数量,请使用PIMPL idiom 等方法。
template class DLL_EXPORT std::allocator<tCharGlyphProviderRef>;
这确实会在当前编译单元中创建模板特化的实例化。所以这会在dll中创建std::allocator的方法并导出相应的方法。这不适用于具体类,因为这只是模板类的实例化。
【讨论】:
谢谢克里斯 - 在这种情况下,还有一个额外的问题是那些标准容器只是标题 - 所以我不确定我们是否需要通过这些成员上的 dll 调用函数只要这些功能在客户端和 dll 中都是相同的 - 长话短说我的不直接暴露给客户端,所以我将禁用警告:) - 但你确定禁用它们会伤害即使它们暴露给定情况如何? 顺便说一句 - 定义类是私有的,因此它们不能被触及存在一个问题 - 这不考虑复制/创建/销毁这些对象 - 所以我们可以说我们可以禁用仅当客户端无法直接在我们的主对象上执行这些操作时才发出警告。在这种情况下,尽管我几乎可以肯定我们根本没有任何问题,因为这些函数对客户端可用,就像它们对 dll 一样——通过编译器标准头文件——对吗? :) 我认为模板类可能不会暴露这些问题,因为方法在客户端实现模块中被实例化。这也适用于内联函数。但我不知道是否所有问题都会消失,因为一些模板调用非模板代码(例如 std::exception::what)。 “这不考虑复制/创建/销毁”是的,如前所述。但是声明这些操作确实可以规避这些问题。【参考方案2】:该警告告诉您,您的 DLL 的用户将无法跨 DLL 边界访问您的容器成员变量。显式导出它们使它们可用,但这是个好主意吗?
一般来说,我会避免从您的 DLL 中导出标准容器。如果您可以绝对保证您的 DLL 将与相同的运行时和编译器版本一起使用,那么您将是安全的。您必须确保在您的 DLL 中分配的内存是使用相同的内存管理器释放的。否则,充其量只能在运行时断言。
因此,不要跨 DLL 边界直接公开容器。如果您需要公开容器元素,请通过访问器方法进行。在您提供的情况下,将接口与实现分开,并在 DLL 级别公开接口。您对 std 容器的使用是您的 DLL 客户端不需要访问的实现细节。
或者,按照 Neil 的建议,创建一个静态库而不是 DLL。您失去了在运行时加载库的能力,并且您的库的使用者必须在您更改库时重新链接。如果这些是您可以接受的折衷方案,那么静态库至少可以让您解决这个问题。我仍然认为您不必要地公开了实现细节,但这可能对您的特定库有意义。
【讨论】:
【参考方案3】:还有其他问题。
有些 STL 容器可以“安全”导出(例如矢量),而有些则不是(例如地图)。
例如 Map 是不安全的,因为它(无论如何在 MS STL 发行版中)包含一个名为 _Nil 的静态成员,它的值在迭代中进行比较以测试是否结束。使用 STL 编译的每个模块都有不同的 _Nil 值,因此在一个模块中创建的映射不能从另一个模块迭代(它永远不会检测到结束并崩溃)。
即使你静态链接到一个库,这也适用,因为你永远无法保证 _Nil 的值是什么(它是未初始化的)。
我相信 STLPort 不会这样做。
【讨论】:
感谢您提供详细信息 - 就我个人而言,我一直更喜欢 STLPort,现在我将有另一个理由:D【参考方案4】:我发现处理这种情况的最佳方法是:
创建您的库,使用库名称中包含的编译器和 stl 版本对其进行命名,就像 boost 库一样。
例子:
- FontManager-msvc10-mt.dll 用于 dll 版本,特定于 MSVC10 编译器,默认 stl。
- FontManager-msvc10_stlport-mt.dll 用于 dll 版本,特定于 MSVC10 编译器,带有 stl 端口。
- FontManager-msvc9-mt.dll 用于 dll 版本,特定于 MSVC 2008 编译器,带有默认 stl
- libFontManager-msvc10-mt.lib 用于静态库版本,特定于 MSVC10 编译器,具有默认 stl。
遵循此模式,您将避免与不同 stl 实现相关的问题。请记住,vc2008 中的 stl 实现与 vc2010 中的 stl 实现不同。
使用 boost::config 库查看您的示例:
#include <boost/config.hpp>
#ifdef BOOST_MSVC
# pragma warning( push )
# pragma warning( disable: 4251 )
#endif
class DLL_EXPORT FontManager
public:
std::map<int, std::string> int2string_map;
#ifdef BOOST_MSVC
# pragma warning( pop )
#endif
【讨论】:
【参考方案5】:似乎很少有人考虑的一种替代方法是根本不使用 DLL,而是静态链接到静态 .LIB 库。如果你这样做,所有导出/导入的问题都会消失(尽管如果你使用不同的编译器,你仍然会遇到名称修改问题)。您当然会失去 DLL 架构的特性,例如函数的运行时加载,但在许多情况下,这可能是一个很小的代价。
【讨论】:
如果你的 DLL 接口使用 C++ 类——尤其是如果你使用标准库类型,你真的不能混合编译器。 给猫剥皮的方法有很多种,如果这是一种选择,我同意它应该采用。【参考方案6】:找到this article。简而言之,亚伦在上面给出了“真实”的答案;不要跨库边界公开标准容器。
【讨论】:
谢谢 Chris - 这真的是一篇很棒的文章,基本上很好地总结了这个线程 - 看起来我们(禁用它并假设客户端和 dll 之间的编译环境兼容,或者使用 lib 而不是 dll,或者不要使用导出类中的模板)并且以上都不是正确的:(【参考方案7】:虽然这个帖子很老了,但我最近发现了一个问题,这让我再次考虑在我的导出类中使用模板:
我写了一个类,它有一个 std::map 类型的私有成员。一切运行良好,直到它在发布模式下编译,即使在构建系统中使用,这也确保所有目标的所有编译器设置都是相同的。地图完全隐藏,没有任何东西直接暴露给客户。
因此,代码只是在发布模式下崩溃。我猜是因为为实现和客户端代码创建了不同的二进制 std::map 实例。
我猜 C++ 标准并没有说明如何处理导出的类,因为这几乎是特定于编译器的。所以我想最大的可移植性规则是只公开接口并尽可能使用 PIMPL 习惯用法。
多谢指教
【讨论】:
这实际上是我在设计界面时使用的最佳实践,最大的额外好处是我们也不会在模块之间创建依赖关系,并且每个模块都可以免费使用他们想要的任何容器库。【参考方案8】:从 dll 中导出包含 std:: 对象(向量、地图等)的类
另请参阅 Microsoft 的 KB 168958 文章 How to export an instantiation of a Standard Template Library (STL) class and a class that contains a data member that is an STL object。来自文章:
导出 STL 类
在 DLL 和 .exe 文件中,链接到 C 运行时的相同 DLL 版本。要么与 Msvcrt.lib (发布版本)链接要么 将两者都与 Msvcrtd.lib 链接(调试版本)。 在 DLL 中,在模板实例化声明中提供 __declspec 说明符,以便从中导出 STL 类实例化 DLL。 在 .exe 文件中,在模板实例化声明中提供 extern 和 __declspec 说明符以从 动态链接库。这会导致警告 C4231“使用非标准扩展: 'extern' 在模板显式实例化之前。”你可以忽略这个 警告。
还有:
导出包含作为 STL 对象的数据成员的类
在 DLL 和 .exe 文件中,链接到 C 运行时的相同 DLL 版本。要么与 Msvcrt.lib (发布版本)链接要么 将两者都与 Msvcrtd.lib 链接(调试版本)。 在 DLL 中,在模板实例化声明中提供 __declspec 说明符,以便从中导出 STL 类实例化 DLL。 注意:您不能跳过第 2 步。您必须导出 用于创建数据成员的 STL 类的实例化。 在 DLL 中,在类的声明中提供 __declspec 说明符以从 DLL 中导出类。 在 .exe 文件中,在类的声明中提供 __declspec 说明符以从 DLL 导入类。如果 您要导出的类有一个或多个基类,那么您必须 也导出基类。 如果您要导出的类 包含属于类类型的数据成员,则必须导出 数据成员的类也是如此。
【讨论】:
【参考方案9】:在这种情况下,请考虑使用 pimpl 成语。将所有复杂类型隐藏在单个 void* 后面。编译器通常不会注意到您的成员是私有的并且所有方法都包含在 DLL 中。
【讨论】:
【参考方案10】:如果您使用 DLL,请在“DLL PROCESS ATTACH”事件中初始化所有对象并导出指向其类/对象的指针。 您可以提供特定的函数来创建和销毁对象以及获取所创建对象的指针的函数,因此您可以将这些调用封装在包含文件的访问包装类中。
【讨论】:
【参考方案11】:由于模板类中的静态数据成员(如 stl 容器),MSVC 不接受上述任何变通方法
每个模块 (dll/exe) 都有自己的每个静态定义的副本...哇!如果您以某种方式“导出”此类数据(如上面“指出”),这将导致可怕的事情......所以不要在家里尝试这个
见http://support.microsoft.com/kb/172396/en-us
【讨论】:
【参考方案12】:在这种情况下使用的最佳方法是使用 PIMPL 设计模式。
【讨论】:
以上是关于从 DLL 导出包含 `std::` 对象(矢量、地图等)的类的主要内容,如果未能解决你的问题,请参考以下文章