met*_*tal 7 c++ c++-standard-library c++11 c++14
对于在许多工具链API/ABI兼容性与相同的二进制,这是很好 众所周知 的是 STL容器,std::string和其他标准库类,如输入输出流被禁止的公共报头.(例外情况是,如果为每个受支持的工具链版本分配一个构建;一个为最终用户编译提供没有二进制文件的源,在本例中不是首选选项;或者一个转换为其他一些内联容器,以便不同的std实现不会被库提取.)
如果已经有一个已发布的库API没有遵循此规则(请求朋友),那么最好的路径是什么,同时保持尽可能多的向后兼容性,并且我不能支持编译时断点?我需要支持Windows和Linux.
重新考虑我正在寻找的ABI兼容性水平:我不需要它是疯狂的未来证明.我主要是为每个版本的多个流行的Linux发行版只做一个库二进制文件.(目前,我为每个编译器发布一个,有时为特殊发行版(RHEL vs Debian)发布特殊版本.与MSVC版本相同的问题 - 所有支持的MSVC版本的一个DLL将是理想的.)其次,如果我不'在破解修复版本中打破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 );
Run Code Online (Sandbox Code Playgroud)
从理论上讲,我们可以将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
Run Code Online (Sandbox Code Playgroud)
大多数客户端代码没有做出异常的事情,比如获取地址,Foo无需修改即可运行.同样,我们可以创建自己的std::vector替代品,但在某些情况下可能会导致复制处罚.
Abseil的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 );
};
Run Code Online (Sandbox Code Playgroud)
现在,旧的虚函数的覆盖将在编译时中断,但调用std::string将自动转换.覆盖应该使用新版本,并在编译时中断.
这里有任何提示或陷阱吗?
我不知道如何处理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 );
}
Run Code Online (Sandbox Code Playgroud)
如果我创建ToString()了一个虚函数,那么我可以迭代所有Bar对象并调用用户的覆盖,因为它只依赖于MyString对象,这些对象在头部定义,它们与std对象(如流)交互.
想法,陷阱?
使用良好的字符串视图。
不要使用std::string const&虚拟重载;没有理由这样做。无论如何,你正在破坏 ABI。一旦他们重新编译,他们将看到新的基于字符串视图的重载,除非他们正在获取和存储指向虚拟函数的指针。
要在不进入中间字符串的情况下进行流式传输,请使用连续传递样式:
void CPS_to_string( Bar const& bar, MyFunctionView< void( MyStringView ) > cps );
Run Code Online (Sandbox Code Playgroud)
wherecps会使用部分缓冲区重复调用,直到对象被序列化出来。写<<在上面(内嵌在标题中)。函数指针间接寻址会带来一些不可避免的开销。
现在只在接口中使用虚拟,并且永远不会重载虚拟方法,并且始终在 vtable 末尾添加新方法。所以不要暴露复杂的层次结构。扩展 vtable 是 ABI 安全的;添加到中间则不然。
FunctionView 是一个简单的手动非拥有 std 函数克隆,其状态为 a void*,并且 aR(*)(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;
};
Run Code Online (Sandbox Code Playgroud)
这使您可以通过 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;
}
Run Code Online (Sandbox Code Playgroud)
并在标题中实现ostream& << MyStringView const&。
将标头中 C++ API 的每个操作转发到extern "C"纯 C 函数(即将 StringView 作为一对char const*指针传递)。仅导出一extern "C"组符号。现在,符号修改更改不再破坏 ypur ABI。
C ABI 比 C++ 更稳定,通过强制您将库调用分解为“C”调用,您可以使 ABI 重大更改变得明显。使用 C++ 头胶使一切变得干净,使用 C 使 ABI 坚如磐石。
如果您愿意冒险,您可以保留纯虚拟接口;使用与上面相同的规则(简单的层次结构,无重载,仅添加到末尾),您将获得良好的 ABI 稳定性。