使用按引用传递而不是按指针传递时的二进制兼容性

Binary compatibility when using pass-by-reference instead of pass-by-pointer

本文关键字:兼容性 二进制 指针 按引用传递      更新时间:2023-10-16

这个问题旨在作为这个问题的后续问题:C++中指针变量和引用变量有什么区别?

在阅读了答案和我在stackoverflow上找到的一些进一步讨论后,我知道编译器应该像对待逐个指针一样对待逐个引用,并且引用只不过是语法糖。考虑到二进制兼容性,我还没有弄清楚是否有任何区别。

在我们的(多平台(框架中,我们要求在发布和调试版本之间(以及框架的不同版本之间(进行二进制兼容。特别是,我们在调试模式下构建的二进制文件必须可用于发布版本,反之亦然。为了实现这一点,我们只在接口中使用纯抽象类和 POD。

请考虑以下代码:

class IMediaSerializable
{
public:
    virtual tResult Serialize(int flags,
                              ISerializer* pSerializer,
                              IException** __exception_ptr) = 0;
//[…]
};

ISerializerIException也是纯抽象类。 ISerializer必须指向现有对象,因此我们始终必须执行 NULL 指针检查。 IException实现了某种异常处理,其中指针指向的地址必须更改。出于这个原因,我们使用指针到指针,也必须检查 NULL 指针。

为了使代码更清晰并摆脱一些不必要的运行时检查,我们希望使用按引用传递重写此代码。

class IMediaSerializable
{
public:
    virtual tResult Serialize(int flags,
                              ISerializer& pSerializer,
                              IException*& __exception_ptr) = 0;
//[…]
};

这似乎没有任何缺陷。但是问题仍然是我们,这是否仍然满足二进制兼容性的要求。

更新:澄清一下:这个问题不是关于代码的按指针传递版本和按引用传递版本之间的二进制兼容性。我知道这不可能是二进制兼容的。事实上,我们有机会重新设计我们的 API,我们考虑使用按引用传递而不是按指针传递,而不关心二进制兼容性(新的主要版本(。问题只是关于仅使用代码的按引用传递版本时的二进制兼容性。

二进制 ABI 兼容性取决于您使用的任何编译器。C++标准不包括二进制 ABI 兼容性问题。

您需要检查C++编译器的文档,以了解它对二进制兼容性的看法。

通常,引用是作为底层指针实现的,因此通常会有 ABI 兼容性。 您必须检查特定编译器的文档以及可能的实现以确保。

但是,在 C++11 岁时,您对纯抽象类和 POD 类型的限制过于热心。

C++11将豆荚的概念分成多个部分。 标准布局涵盖了 Pod 类型的大多数(如果不是全部("内存布局"保证。

但标准布局类型可以具有构造函数和析构函数(以及其他差异(。

所以你可以做一个非常友好的界面。

编写一个简单的智能指针,而不是手动管理的接口指针。

template<class T>
struct value_ptr {
  T* raw;
  // ...      
};

->clone()在复制时,在移动时移动指针,在销毁时删除,并且(因为你拥有它(可以保证在编译器库修订版中稳定(而unique_ptr不能(。 这基本上是一个支持->clone() unique_ptr。 对于无法复制的值,您还要有自己的unique_ptr

现在,您可以将纯虚拟接口替换为一对类型。 首先是纯虚拟接口(通常带有T* clone() const(,其次是常规类型:

struct my_regular_foo {
  value_ptr< IFoo > ptr;
  bool some_method() const { return ptr->some_method(); } // calls pure virtual method in IFoo
};

最终结果是,您有一个行为类似于常规日常类型的类型,但它是作为纯虚拟接口类的包装器实现的。 这些类型可以按值获取、按引用获取和按值返回,并且可以在其中保存任意复杂状态。

这些类型位于库公开的头文件中。

并且IFoo的接口扩展很好。 只需在类型末尾向两个IFoo添加一个新方法(在大多数 ABI 下是向后兼容的 (!( - 尝试一下(,然后添加一个新方法来my_regular_foo转发到它。 由于我们没有更改my_regular_foo的布局,即使库和客户端代码可能不同意它的方法,这很好 - 这些方法都是内联编译的,从未导出 - 知道他们正在使用新版本库的客户端可以使用它,而那些不知道但正在使用它的人很好(无需重建(。

有一个谨慎的问题:如果将重载添加到方法的IFoo(不是覆盖:重载(,则虚拟方法的顺序会更改,并且如果添加新的父virtual,则虚拟表的布局可能会更改,并且仅当抽象类的所有继承都virtual在公共 API 中时(使用虚拟继承, vtable 具有指向子类的每个 vtable 开头的指针:因此每个子类可以具有更大的 vtable,而不会弄乱地址其他函数虚函数。 如果你小心翼翼地只附加到一个子类的末尾,使用早期的头文件仍然可以找到早期的方法(。

最后一步 - 允许在接口上使用新方法 - 可能是通往远方的桥梁,因为您必须调查每个支持的编译器的vtable布局的ABI保证(在实践中和不是(。

不,无论您使用哪种编译器,它都不起作用。

考虑一个导出两个函数的类 Foo:

class Foo
{
public:
     void f(int*);
     void f(int&);
};

编译器必须将f的两个函数的名称转换(篡改(为特定于 ABI 的字符串,以便链接器可以区分两者。

现在,由于编译器需要支持重载解析,即使引用的实现方式与指针完全相同,这两个函数名称也需要具有不同的重整名称。

例如,GCC 将这些名称修改为:

void Foo::f(int*) => _ZN3Foo1fEPi
void Foo::f(int&) => _ZN3Foo1fERi

注意 P vs R .

因此,如果您更改函数的签名,您的应用程序将无法链接。