在库的公共API

时间:2018-03-01 22:25:50

标签: c++ c++11 c++14 c++-standard-library

对于具有相同二进制文件的许多工具链中的API / ABI兼容性,well known that STL容器,std::string和其他标准库类(如iostream)是公共标题中的 verboten 。 (例外情况是,如果为每个受支持的工具链版本分配一个构建;一个为最终用户编译提供没有二进制文件的源,在本例中不是首选选项;或者一个转换为其他一些内联容器,以便不同的std实现不会被库摄取。)

如果已经有一个已发布的库API没有遵循此规则(请求朋友),那么最好的路径是什么,同时保持尽可能多的向后兼容性,并且有利于编译时断点我可以&# 39;吨?我需要支持Windows和Linux。

重新确定我正在寻找的ABI兼容性水平:我不需要它是疯狂的面向未来的。我主要希望为每个版本的多个流行的Linux发行版只做一个库二进制文件。 (目前,我为每个编译器发布一个,有时是特殊发行版的特殊版本(RHEL vs Debian)。与MSVC版本相同的问题 - 所有支持的MSVC版本的一个DLL将是理想的。)其次,如果我不&# 39;在破解版本中打破API,我希望它与ABI兼容,并且无需重建客户端应用程序即可替换掉DLL / SO。

我有三个案例有一些初步的建议,模仿Qt到一定程度。

旧的公共API:

// Case 1: Non-virtual functions with containers
void Foo( const char* );
void Foo( const std::string& );

// Case 2: Virtual functions
class Bar
{
public:
    virtual ~Bar() = default;
    virtual void VirtFn( const std::string& );
};

// Case 3: Serialization
std::ostream& operator << ( std::ostream& os, const Bar& bar );

案例1:带容器的非虚函数

理论上,我们可以将std::string用法转换为非常类似std::string_view的类,但在我们库的API / ABI控件下。它将在我们的库头中从std::string转换,以便编译的库仍然可以接受,但独立于std::string实现,并且向后兼容:

新API:

class MyStringView
{
public:
    MyStringView( const std::string& ) // Implicit and inline
    {
        // Convert, possibly copying
    }

    MyStringView( const char* ); // Implicit
    // ...   
};

void Foo( MyStringView ); // Ok! Mostly backwards compatible

大多数未执行异常操作的客户端代码(如获取Foo的地址)无需修改即可运行。同样,我们可以创建自己的std::vector替换,但在某些情况下可能会导致复制惩罚。

Abseil's ToW #1建议从util代码开始,然后进行操作,而不是从API开始。还有其他任何提示或陷阱吗?

案例2:虚拟功能

但虚拟功能怎么样?如果我们更改签名,我们会破坏向后兼容性。我想我们可以使用final将原来的旧版本留下来强制破坏:

// Introduce base class for functions that need to be final
class BarBase
{
public:
    virtual ~BarBase() = default;
    virtual void VirtFn( const std::string& ) = 0;
};

class Bar : public BarBase
{
public:
    void VirtFn( const std::string& str ) final
    {
        VirtFn( MyStringView( str ) );
    }

    // Add new overload, also virtual
    virtual void VirtFn( MyStringView );
};

现在,旧的虚函数的覆盖将在编译时中断,但使用std::string的调用将自动转换。覆盖应该使用新版本,并在编译时中断。

这里有任何提示或陷阱吗?

案例3:序列化

我不确定如何处理iostreams。一种选择,存在一些低效率的风险,是将它们内联定义并通过字符串重新路由它们:

MyString ToString( const Bar& ); // I control this, could be a virtual function in Bar if needed

// Here I publicly interact with a std object, so it must be inline in the header
inline std::ostream& operator << ( std::ostream& os, const Bar& bar )
{
    return os << ToString( bar );
}

如果我使ToString()成为一个虚函数,那么我可以遍历所有Bar对象并调用用户的覆盖,因为它只依赖于MyString对象,这些对象是在与它们交互的标题中定义的像流一样的std对象。

思考,陷阱?

2 个答案:

答案 0 :(得分:1)

第1层

使用良好的字符串视图。

不要使用std::string const&虚拟重载;没有理由。无论如何,你正打破ABI。一旦他们重新编译,他们将看到新的基于字符串视图的重载,除非他们正在获取并存储指向虚函数的指针。

要在不使用中间字符串的情况下进行流式传输,请使用continuation传递样式:

void CPS_to_string( Bar const& bar, MyFunctionView< void( MyStringView ) > cps );

其中使用部分缓冲区重复调用cps,直到将对象序列化为止。在其上面写<<(在标题中内联)。函数指针间接有一些不可避免的开销。

现在只使用虚拟接口,永远不会重载虚方法,并始终在vtable的末尾添加新方法。因此,不要暴露复杂的heirarchies。扩展vtable是ABI安全的;添加到中间不是。

FunctionView是一个简单的手动非拥有std函数克隆,其状态为void*R(*)(void*,args&&...),它应该是ABI稳定的,以便跨越库边界。

template<class Sig>
struct FunctionView;

template<class R, class...Args>
struct FunctionView<R(Args...)> {
  FunctionView()=default;
  FunctionView(FunctionView const&)=default;
  FunctionView& operator=(FunctionView const&)=default;

  template<class F,
    std::enable_if_t<!std::is_same< std::decay_t<F>, FunctionView >{}, bool> = true,
    std::enable_if_t<std::is_convertible< std::result_of_t<F&(Args&&...)>, R>, bool> = true
  >
  FunctionView( F&& f ):
    ptr( std::addressof(f) ),
    f( [](void* ptr, Args&&...args)->R {
      return (*static_cast< std::remove_reference_t<F>* >(ptr))(std::forward<Args>(args)...);
    } )
  {}
private:
  void* ptr = 0;
  R(*f)(void*, Args&&...args) = 0;
};
template<class...Args>
struct FunctionView<void(Args...)> {
  FunctionView()=default;
  FunctionView(FunctionView const&)=default;
  FunctionView& operator=(FunctionView const&)=default;

  template<class F,
    std::enable_if_t<!std::is_same< std::decay_t<F>, FunctionView >{}, bool> = true
  >
  FunctionView( F&& f ):
    ptr( std::addressof(f) ),
    f( [](void* ptr, Args&&...args)->void {
      (*static_cast< std::remove_reference_t<F>* >(ptr))(std::forward<Args>(args)...);
    } )
  {}
private:
  void* ptr = 0;
  void(*f)(void*, Args&&...args) = 0;
};

这使您可以通过API障碍传递通用回调。

// f can be called more than once, be prepared:
void ToString_CPS( Bar const& bar, FunctionView< void(MyStringView) > f );
inline std::ostream& operator<<( std::ostream& os, const Bar& bar )
{
  ToString_CPS( bar, [&](MyStringView str) {
    return os << str;
  });
  return os;
}

并在标题中实施ostream& << MyStringView const&

第2层

将标头中的C ++ API中的每个操作转发到extern "C" pure-C函数(即将StringView作为一对char const* ptrs传递)。仅导出extern "C"个符号集。现在,符号改变不再破坏ypur ABI。

C ABI比C ++更稳定,并且通过强制您将库调用分解为“C”调用,可以使ABI明显改变。使用C ++标题胶将事情清理干净,C使ABI坚如磐石。

如果您愿意冒险,可以保留纯虚拟接口;使用与上面相同的规则(简单的heirarchies,没有重载,只添加到最后),你将获得体面的ABI稳定性。

答案 1 :(得分:1)

集装箱

为了接受字符串和数组作为函数参数,分别使用std::string_viewgsl::span,或者使用稳定的ABI。非contiguos容器可以作为any_iterator的范围传递。

通过引用返回,您可以再次使用这些类。

对于按值返回sting,您可以将std::string_view返回给线程本地全局对象,该对象在下一次API调用之前有效(如std::ctime函数)。如有必要,用户必须进行深层复制。

要按值返回容器,您可以使用基于回调的API。您的API将为要返回的容器的每个元素调用用户回调。

std::string_viewgsl::spanany_iterator或其等效项必须在您的资源库附带给其用户的头文件中实现。

虚拟功能

您可以在库的API中使用Pimpl习语而不是具有虚函数的类。

序列化

可以在头库文件中实现,这些头文件使用库的公共API,并使用IOStreams进行序列化/反序列化。