【问题标题】:Binary compatibility when using pass-by-reference instead of pass-by-pointer使用传递引用而不是传递指针时的二进制兼容性
【发布时间】:2026-02-15 19:30:01
【问题描述】:

此问题旨在作为此问题的后续问题:What are the differences between a pointer variable and a reference variable in C++?

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

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

考虑以下代码:

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

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

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

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

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

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

【问题讨论】:

  • 即使它是二进制兼容的,函数名也会有不同的错位,因此它不会链接。
  • 澄清了问题,我希望问题现在清楚了。
  • POD 在 C++11 中消失了(完全分开):现在我们有了标准布局,它允许在课堂上发挥很大的作用。您可以像 POD 类型一样可靠地传递正则化的复杂对象。

标签: c++ pass-by-reference binary-compatibility pass-by-pointer interface-design


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

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

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

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

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

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

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

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

    注意PR

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

    【讨论】:

    • 澄清了问题,我希望问题现在清楚了。
    【解决方案3】:

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

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

    C++11 将 pod 的概念拆分为多个部分。标准布局涵盖了大多数(如果不是全部)pod 类型的“内存布局”保证。

    但标准布局类型可以有构造函数和析构函数(还有其他区别)。

    这样您就可以制作一个非常友好的界面。

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

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

    -&gt;clone()s 在复制时,在移动时移动指针,在销毁时删除,并且(因为你拥有它)可以保证在编译器库修订版中是稳定的(而 unique_ptr 不能)。这基本上是一个支持-&gt;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父类的布局虚拟表可以更改,并且只有在您的公共 API 中对抽象类的所有继承都是 virtual 时才可靠地工作(使用虚拟继承,vtable 具有指向子类的每个 vtable 开头的指针:所以每个子-class 可以有一个更大的 vtable 而不会弄乱其他函数的地址 虚函数。如果你仔细地只附加到子类的末尾 vtable 代码使用早期的头文件仍然可以找到早期的方法。

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

    【讨论】:

    • 我希望我能更深入地了解 C++11 中的通用 POD,它会首先解决我们的一些 API 问题。要了解更通用的 POD 并对您的建议有更深入的了解,我阅读了 Wikipedia 和 Bjarnes 上的一些基础知识,非常出色 C++11 FAQ。感谢您的回答,深入挖掘。