6

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

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

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

考虑以下代码:

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

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

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

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

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

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

4

3 回答 3

6

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

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

于 2015-02-12T13:28:03.907 回答
3

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

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

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

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

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

代替手动管理的接口指针,编写一个简单的智能指针。

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

->clone()是在复制时,在移动时移动指针,在销毁时删除,并且(因为你拥有它)可以保证在编译器库修订版中是稳定的(虽然unique_ptr不能)。这基本上是一个unique_ptr支持->clone(). 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代码,使用早期的头文件仍然可以找到早期的方法)。

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

于 2015-02-12T15:46:16.537 回答
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

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

于 2015-02-12T14:04:41.267 回答