在库的公共API中从std :: string,std :: ostream等过渡 [英] Transitioning away from std::string, std::ostream, etc. in a library's public API

查看:63
本文介绍了在库的公共API中从std :: string,std :: ostream等过渡的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

对于具有相同二进制文件的许多工具链中的API / ABI兼容性,它很好 已知 认为 STL容器, std :: string 和其他标准库类(例如iostream)是 verboten 在公共标题中。 (以下情况例外:如果一个人为每个受支持的工具链版本分发一个版本;一个人提供的源文件没有二进制文件供最终用户编译,这在当前情况下不是首选选项;或者一个人转换为其他一些内联容器,以便

For API/ABI compatibility across many toolchains with the same binary, it is well known that STL containers, std::string, and other standard library classes like iostreams are verboten in public headers. (Exceptions to this are if one is distributing one build for each version of supported toolchains; one delivers source with no binaries for end-user compilation, which are not preferred options in the present case; or one translates to some other container inline so that a differing std implementation doesn't get ingested by the library.)

如果一个人已经拥有不遵循此规则的已发布库API(向朋友求助),最好的前进方向是什么,同时尽可能地保持向后兼容性,并在无法做到的情况下支持编译时中断?我需要支持Windows和Linux。

If one already had a published library API that did not follow this rule (asking for a friend), what is the best path forward while maintaining as much backwards compatibility as I reasonably can and favoring compile-time breakages where I can't? I need to support Windows and Linux.

重新设置我要寻找的ABI兼容性:我并不需要它具有永不过时的能力。我主要是希望每个发行版只为多个流行的Linux发行版提供一个库二进制文件。 (目前,我为每个编译器发布一个版本,有时为一个特殊发行版发布一个特殊版本(RHEL与Debian)。MSVC版本也有类似的问题-所有支持的MSVC版本都应该有一个DLL。)为了在一个错误修正版本中破坏该API,我希望它能够与ABI兼容并且可以直接替换DLL / SO,而无需重建客户端应用程序。

Re the level of ABI compatibility I'm looking for: I don't need it to be insanely future-proof. I'm mainly looking to do just one library binary for multiple, popular Linux distros per release. (At present, I release one per compiler and sometimes special versions for a special distro (RHEL vs Debian). Same sort of concerns with MSVC versions -- one DLL for all supported MSVC versions would be ideal.) Secondarily, if I don't break the API in a bugfix release, I would like it to be ABI-compatible and a drop-in DLL/SO replacement without rebuilding the client application.

旧的公共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 实现并向后兼容:

Case 1: Non-virtual functions with containers

In theory we can convert std::string uses to a class very much like std::string_view but under our library's API/ABI control. It will convert within our library header from a std::string so that the compiled library still accepts but is independent of the std::string implementation and is backwards compatible:

新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 替换,尽管在某些情况下可能会导致复制损失。

Most client code that is not doing something abnormal like taking the address of Foo will work without modification. Likewise, we can create our own std::vector replacement, though it may incur a copying penalty in some cases.

Abseil的ToW#1 建议从util代码开始而不是从API开始。还有其他提示或陷阱吗?

Abseil's ToW #1 recommends starting at the util code and working up instead of starting at the API. Any other tips or pitfalls here?

但是虚函数呢?如果更改签名,则会破坏向后兼容性。我想我们可以用 final 将旧的保留在原处以强制破损:

But what about virtual functions? We break backwards compatibility if we change the signature. I suppose we could leave the old one in place with final to force breakage:

// 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 );
};

现在,对旧虚拟函数的覆盖将在编译时中断,但使用<$ c $进行调用c> std :: string 将自动转换。覆盖应该改用新版本,并且会在编译时中断。

Now an override of the old virtual function will break at compile-time but calls with std::string will be automagically converted. Overrides should use the new version instead and will break at compile-time.

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

Any tips or pitfalls here?

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

I'm not sure what to do with iostreams. One option, at the risk of some inefficiency, is to define them inline and reroute them through strings:

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对象进行交互。

If I made ToString() a virtual function, then I can iterate over all Bar objects and call the user's overrides because it only depends on MyString objects, which are defined in the header where they interact with std objects like the stream.

想法,陷阱吗?

推荐答案

第1层



使用良好的字符串视图。

Tier 1

Use a good string view.

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

Don't use a std::string const& virtual overload; there is no reason for it. You are breaking ABI anyhow. Once they recompile, they'll see the new string-view based overload, unless they are taking and storing pointers to virtual functions.

无需进行中间字符串使用即可进行流传输连续传递样式:

To stream without going to intermediate string use continuation passing style:

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

其中 cps 被局部缓冲区重复调用直到对象被序列化为止。在其上写<< (在标题中内联)。函数指针间接化会带来不可避免的开销。

where cps is repeatedly called with partial buffers until object is serialized out it. Write << on top of that (inline in headers). There is some unavoidable overhead from function pointer indirection.

现在仅在接口中使用虚函数,从不重载虚方法,并且始终在vtable的末尾添加新方法。因此,请勿公开复杂的层次结构。扩展vtable是ABI安全的;

Now only use virtual in interfaces and never overload virtual methods and always add new methods at the end of the vtable. So don't expose complex heirarchies. Extending a vtable is ABI safe; adding to the middle is not.

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

FunctionView is a simple hand rolled non-owning std function clone whose state is a void* and a R(*)(void*,args&&...) which should be ABI stable to pass across library boundry.

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屏障传递通用回调。

this lets you pass generic callbacks over your API barrier.

// 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&

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

Forward every operation from a C++ API in headers to extern "C" pure-C functions (ie pass StringView as a pair of char const* ptrs). Export only an extern "C" set of symbols. Now symbol mangling changes no longer breaks ypur ABI.

C ABI比C ++更稳定,通过强制将库调用分解为 C调用,您可以进行ABI的突破性变化显而易见。

C ABI is more stable than C++, and by forcing you to break library calls down into "C" calls you can make ABI breaking changes obvious. Use C++ header glue to make things clean, C to make ABI rock solid.

如果愿意冒险,可以保留纯虚拟接口;使用C ++标头胶可以使事物清洁,使用C可以使ABI坚如磐石。使用与上述相同的规则(简单的层次结构,没有重载,仅添加到末尾),您将获得不错的ABI稳定性。

You can keep your pure virtual interfaces if you are willing to risk it; use the same rules as above (simple heirarchies, no overloads, only add to the end) and you'll get decent ABI stability.

这篇关于在库的公共API中从std :: string,std :: ostream等过渡的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆