【问题标题】:Safely use containers in C++ library interface在 C++ 库接口中安全使用容器
【发布时间】:2014-02-06 18:25:41
【问题描述】:

在设计 C++ 库时,我了解到在公共接口中包含 std::vector 之类的标准库容器是一种不好的做法(参见例如 Implications of using std::vector in a dll exported function)。

如果我想公开一个接受或返回对象列表的函数怎么办?我可以使用一个简单的数组,但是我必须添加一个count 参数,这使得界面更麻烦,更不安全。例如,如果我想使用map,它也无济于事。我猜像 Qt 之类的库定义了自己的可以安全导出的容器,但我宁愿不添加 Qt 作为依赖项,也不想滚动自己的容器。

在库界面中处理容器的最佳做法是什么?是否有可以用作“胶水”的小型容器实现(最好只有一个或两个我可以放入的文件,具有许可的许可证)?或者有没有办法让std::vector 等跨 .DLL/.so 边界并使用不同的编译器安全?

【问题讨论】:

    标签: c++ containers api-design


    【解决方案1】:

    实际上,这不仅适用于 STL 容器,而且适用于几乎所有 C++ 类型(尤其是所有其他标准库类型)。

    由于 ABI 不标准化,您可能会遇到各种麻烦。通常,您必须为每个受支持的编译器版本提供单独的二进制文件才能使其工作。获得真正可移植的 DLL 的唯一方法是坚持使用纯 C 接口。这通常会导致类似COM 的情况,因为您必须确保所有分配和匹配的解除分配都发生在同一个模块中,并且不会向用户公开实际对象布局的任何细节。

    【讨论】:

    • 也许不是所有标准库类型——我敢打赌,你可以在一堆工具链上使用 std::pair、array、bitset 和 tuple。但这肯定不能保证——我只是在集思广益,stdlib 的哪些部分可能没有毒性。
    • @JohnZwinck:我不会tuple;如果我没记错的话,libstdc++ 和 libc++ 版本差别很大。
    • @ComicSansMS:是的,我只是想到了 COM,想知道他们如何解决那里的问题......这导致了 VARIANT 和 SAFEARRAY 的兔子洞。我对 COM 了解得越多,就越意识到它之所以如此复杂是有原因的。
    • @jdm 这确实是一个很深的兔子洞,也是我避免像瘟疫一样在 C++ 中使用动态库的第一个原因。最简单的解决方案总是以源代码的形式发布您的库,并让客户使用他们的工具链对其进行编译。不幸的是,在许多情况下,这不是一种选择。希望我们最终会在 ISO C++ 中获得一个标准化的模块系统来解决这些问题。
    • @jdm COM 通过定义自己的标准类型来处理它,这些标准类型是由 C 结构构建的。要与非 C++ 代码(以及由不同编译器编译的 C++ 代码!)进行互操作,您需要使用标准化的东西 - C 类型似乎是唯一的选择。
    【解决方案2】:

    您可以实现模板功能。这有两个好处:

    1. 它让您的用户可以决定他们希望在您的界面中使用哪些类型的容器。
    2. 它让您不必担心 ABI 兼容性,因为您的库中没有代码,它将在用户调用函数时被实例化。

    例如,把这个放在你的头文件中:

    template <typename Iterator>
    void foo(Iterator begin, Iterator end)
    {
      for (Iterator it = begin; it != end; ++it)
        bar(*it); // a function in your library, whose ABI doesn't depend on any container
    }
    

    然后您的用户可以使用任何容器类型调用 f​​oo,即使是他们发明的您不知道的容器类型。

    一个缺点是您需要公开实现代码,至少对于 foo。

    编辑:您还说过您可能想要返回一个容器。考虑诸如回调函数之类的替代方案,就像 C 中的黄金时代一样:

    typedef bool(*Callback)(int value, void* userData);
    void getElements(Callback cb, void* userData) // implementation in .cpp file, not header
    {
      for (int value : internalContainer)
        if (!cb(value, userData))
          break;
    }
    

    这是一种相当老派的“C”方式,但它为您提供了一个稳定的界面,并且基本上任何调用者都可以使用(甚至是实际的 C 代码稍作改动)。这两个怪癖是 void* userData 让用户在其中插入一些上下文(例如,如果他们想调用成员函数)和 bool 返回类型让回调告诉你停止。您可以使用 std::function 或其他任何方法使回调更加精美,但这可能会破坏您的其他一些目标。

    【讨论】:

    • 我可以建议“函数模板”而不是“模板函数”吗? :)
    【解决方案3】:

    TL;DR如果您为各种受支持的集合(ABI + 标准库实现)分发源代码或编译的二进制文件,则没有问题。

    一般来说,后者被视为麻烦(有原因),因此是指南。


    我相信在我能扔的范围内挥手的指导方针......我鼓励你也这样做。

    本指南源于 ABI 兼容性问题:ABI 是一组复杂的规范,定义了编译库的确切接口。主要包括:

    • 结构的内存布局
    • 函数的名称修改
    • 函数的调用约定
    • 异常处理、运行时类型信息……
    • ...

    如需了解更多详情,请查看Itanium ABI。与具有非常简单 ABI 的 C 相比,C++ 的表面积要复杂得多……因此为它创建了许多不同的 ABI。

    除了 ABI 兼容性之外,标准库实现也存在问题。大多数编译器都有自己的标准库实现,这些实现彼此不兼容(例如,它们不会以相同的方式表示std::vector,尽管它们都实现了相同的接口和保证)。

    因此,一个已编译的二进制文件(可执行文件或库)只能与另一个已编译的二进制文件混合并匹配,前提是两者都是针对相同的 ABI 并与标准库实现的兼容版本进行编译的。

    干杯:如果您分发源代码并让客户端编译没有问题。

    【讨论】:

    • 你能评论一下这个相关的Q&A吗?
    【解决方案4】:

    如果您使用的是 C++11,则可以使用 cppcomponents。 https://github.com/jbandela/cppcomponents

    这将允许您在使用不同编译器或标准库创建的 Dll/或 .so 文件中使用 std::vector 作为参数或返回值。看看我对类似问题的回答,例如Passing reference to STL vector over dll boundary

    示例注意,您需要在CPPCOMPONENTS_DEFINE_FACTORY() 语句后添加CPPCOMPONENTS_REGISTER(ImplementFiles)

    【讨论】:

      猜你喜欢
      • 2012-05-09
      • 2012-03-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-02-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多