c++ - 在库的公共(public) API 中从 std::string、std::ostream 等过渡

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

对于具有相同二进制文件的多个工具链之间的 API/ABI 兼容性,它是 well known that STL 容器、std::string 和其他标准库类(如 iostreams)在公共(public) header 中禁止。 (异常(exception)情况是,如果一个人为每个版本的受支持工具链分发一个构建;一个人提供没有二进制文件的源代码供最终用户编译,这在当前情况下不是首选选项;或者一个人内联转换为其他容器,以便不同的 std 实现不会被库吸收。)

如果一个人已经有一个不遵循此规则的已发布库 API(请 friend ),那么最好的前进道路是什么,同时保持尽可能多的向后兼容性,并尽可能地支持编译时中断'吨?我需要支持 Windows 和 Linux。

关于我正在寻找的 ABI 兼容性级别:我不需要它是疯狂的面向 future 的。我主要希望为每个版本的多个流行的 Linux 发行版只做一个库二进制文件。 (目前,我为每个编译器发布一个,有时为特殊发行版(RHEL vs Debian)发布特殊版本。与 MSVC 版本有同样的问题——一个用于所有支持的 MSVC 版本的 DLL 将是理想的。)其次,如果我不为了不在错误修复版本中破坏 API,我希望它与 ABI 兼容,并在不重建客户端应用程序的情况下直接替换 DLL/SO。

我有三个案例和一些初步建议,在一定程度上模仿了 Qt。

旧的公共(public) 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 开始。这里还有其他提示或陷阱吗?

案例二:虚函数

但是虚函数呢?如果我们更改签名,我们会破坏向后兼容性。我想我们可以将旧的保留在 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 的调用将自动转换。覆盖应该改用新版本,并且会在编译时中断。

这里有什么提示或陷阱吗?

案例三:序列化

我不确定如何处理 iostream。一种可能会降低效率的选择是内联定义它们并通过字符串重新路由它们:

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 对象,这些对象在它们交互的 header 中定义像流这样的标准对象。

想法,陷阱?

最佳答案

一级

使用良好的字符串 View 。

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

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

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

哪里cps使用部分缓冲区重复调用,直到对象被序列化。写<<最重要的是(标题中的内联)。函数指针间接寻址有一些不可避免的开销。

现在只在接口(interface)中使用虚函数并且从不重载虚方法并且总是在 vtable 的末尾添加新方法。所以不要暴露复杂的层次结构。扩展 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 层

将 header 中来自 C++ API 的每个操作转发到 extern "C"纯 C 函数(即,将 StringView 作为一对 char const* 指针传递)。仅导出 extern "C"符号集。现在符号修改更改不再破坏 ypur ABI。

C ABI 比 C++ 更稳定,通过强制您将库调用分解为“C”调用,您可以使 ABI 的重大变化变得显而易见。使用 C++ header glue 使事情变得干净,使用 C 使 ABI 坚如磐石。

如果您愿意冒险,您可以保留您的纯虚拟接口(interface);使用与上面相同的规则(简单的继承关系,没有重载,只添加到末尾),您将获得不错的 ABI 稳定性。

关于c++ - 在库的公共(public) API 中从 std::string、std::ostream 等过渡,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49059675/

相关文章:

c++ - 函数返回的神秘段错误,不涉及指针或分配

C++测试矩阵是否已经初始化

c++ - 构建wxWidgets Hello world

c++ - 链接时大量 std::functions 的多重定义

c++ - boost::shared_ptr 没有类类型

C++11:带有 std::thread 和 lambda 函数的段错误

c++ - 防止通过引用传递右值

c++ - 现代 C++ 实现具有可变数量的 int 参数的函数的最佳方法

c++ - 如何在 C++ 中提供像 lambda 这样的可调用对象保护访问?

c++ - 可以使用对 shared_ptr 拥有的对象的引用吗?