向后兼容性-在C++中,重写现有的虚拟函数是否会破坏ABI

backwards compatibility - In C++, does overriding an existing virtual function break ABI?

本文关键字:函数 虚拟 是否 ABI 兼容性 C++ 重写      更新时间:2023-10-16

我的库有两个类,一个基类和一个派生类。在当前版本的库中,基类有一个虚拟函数foo((,派生类不会覆盖它。在下一个版本中,我希望派生类覆盖它。这会破坏ABI吗?我知道引入一个新的虚拟函数通常是这样,但这似乎是一个特例。我的直觉是,它应该改变vtbl中的偏移量,而不是实际改变表的大小。

显然,由于C++标准没有强制要求特定的ABI,所以这个问题在某种程度上是特定于平台的,但在实践中,在大多数编译器中,破坏和维护ABI的方法是相似的。我对GCC的行为很感兴趣,但人们能回答的编译器越多,这个问题就越有用;(

可能。

你错了偏移量。vtable中的偏移量已经确定。将发生的情况是,Derived类构造函数将用Derived重写替换该偏移量处的函数指针(通过将类内的v指针切换到新的v表(。因此,它通常与ABI兼容。

但是,由于优化,尤其是函数调用的机会不足,可能会出现问题。

通常,当调用虚拟函数时,编译器会通过vpointer在vtable中引入查找。然而,如果它可以(静态地(推导出对象的确切类型,那么它也可以推导出要调用的确切函数,并减少虚拟查找。

示例:

struct Base {
  virtual void foo();
  virtual void bar();
};
struct Derived: Base {
  virtual void foo();
};
int main(int argc, char* argv[]) {
  Derived d;
  d.foo(); // It is necessarily Derived::foo
  d.bar(); // It is necessarily Base::bar
}

在这种情况下。。。简单地链接到您的新库不会获得Derived::bar

这似乎不是一个通常可以特别依赖的东西——正如您所说,C++ABI非常棘手(甚至包括编译器选项(。

也就是说,我认为您可以在进行更改之前和之后使用g++ -fdump-class-hierarchy来查看父或子vtables的结构是否发生了更改。如果他们不这样做,那么假设你没有违反ABI可能是"相当"安全的。

是的,在某些情况下,添加虚拟函数的重新实现将更改虚拟函数表的布局。如果您从不是第一个基类的基类(多重继承(重新实现虚拟函数,就会出现这种情况:

// V1
struct A { virtual void f(); };
struct B { virtual void g(); };
struct C : A, B { virtual void h(); }; //does not reimplement f or g;
// V2
struct C : A, B {
    virtual void h();
    virtual void g();  //added reimplementation of g()
};

这通过为g()添加一个条目改变了C的vtable的布局(感谢"Gof"在http://marcmutz.wordpress.com/2010/07/25/bcsc-gotcha-reimplementing-a-virtual-function/)。

此外,正如在其他地方提到的,如果库的用户以静态类型等于动态类型的方式使用您要重写的函数的类,则会遇到问题。这可能是在你新买它之后的情况:

MyClass * c = new MyClass;
c->myVirtualFunction(); // not actually virtual at runtime

或在堆栈上创建:

MyClass c;
c.myVirtualFunction(); // not actually virtual at runtime

其原因是一种名为"去虚拟化"的优化。如果编译器能够在编译时证明对象的动态类型是什么,它将不会通过虚拟函数表发出间接性,而是直接调用正确的函数。

现在,如果用户根据旧版本的库进行编译,编译器将插入对虚拟方法的最派生的重新实现的调用。如果在库的较新版本中,您在更派生的类中重写此虚拟函数,则针对旧库编译的代码仍将调用旧函数,而新代码或编译器在编译时无法证明对象的动态类型的代码将通过虚拟函数表。因此,该类的给定实例在运行时可能会遇到对基类函数的调用,而该调用无法拦截,这可能会违反类不变量。

我的直觉是,它应该改变vtbl中的偏移量,而不是实际改变表的大小。

你的直觉显然是错误的:

  • 或者在vtable中有一个新的条目供覆盖者使用,然后移动下面的所有条目,并且表增长
  • 或者没有新条目,并且vtable表示不变

哪一个是真的取决于许多因素。

无论如何:不要指望它。

注意:请参阅在C++中,重写现有的虚拟函数会破坏ABI吗?对于这种逻辑不成立的情况;

在我看来,Mark使用g++-fdump类层次结构的建议将是这里的赢家,在进行了适当的回归测试之后


重写内容不应更改vtable布局[1]。vtable条目本身将在库的数据段中,IMHO,因此对it进行更改应该不会造成问题。

当然,应用程序需要重新链接,否则如果消费者使用直接引用&派生::overriddenMethod;我不确定编译器是否会被允许将其解析为&Base::overriddenMethod,但安全总比抱歉好。

[1] 详细说明:这假设方法一开始是虚拟的