在C++库接口中安全地使用容器

Safely use containers in C++ library interface

本文关键字:安全 C++ 接口      更新时间:2023-10-16

在设计C++库时,我了解到在公共接口中包含std::vector等标准库容器是不好的做法(例如,请参阅在dll导出函数中使用std::vector的含义)。

如果我想公开一个接受或返回对象列表的函数,该怎么办?我可以使用一个简单的数组,但之后我必须添加一个count参数,这会使接口更加繁琐和不安全。此外,例如,如果我想使用map,也不会有多大帮助。我想像Qt这样的库定义了自己的容器,这些容器可以安全导出,但我不想将Qt添加为依赖项,也不想滚动自己的容器。

在库接口中处理容器的最佳做法是什么?有没有一个小的容器实现(最好只有一两个文件,我可以放进去,有许可证)可以用作"胶水"?或者,有没有一种方法可以使std::vector等在.DLL/.so边界上以及使用不同的编译器时都是安全的?

您可以实现一个模板函数。这有两个优点:

  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
}

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

一个缺点是需要公开实现代码,至少对于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::函数或其他什么方法让回调变得更花哨,但这可能会破坏你的其他一些目标。

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

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

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

一般来说,后者被视为繁琐(有原因),因此成为准则。


我相信挥手指导原则,尽我所能。。。我鼓励你们也这样做。

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

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

有关更多详细信息,请查看安腾ABI。与具有非常简单ABI的C相反,C++具有更复杂的表面积。。。因此为其创建了许多不同的ABI

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

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

干杯:如果你分发源代码并让客户端编译,那没问题

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

这将允许您在使用不同编译器或标准库创建的Dll/或.so文件中使用std::vector作为参数或返回值。看看我对类似问题的回答,例如通过dll边界传递对STL向量的引用

注意,对于该示例,您需要在CPPCOMPONENTS_DEFINE_FACTORY()语句之后添加一个CPPCOMPONENTS_REGISTER(ImplementFiles)