13

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

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

4

5 回答 5

11

它可能。

关于偏移量,您错了。vtable 中的偏移量已经确定。将会发生的是,派生类构造函数将用派生覆盖替换该偏移处的函数指针(通过将类内 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

于 2011-04-21T15:21:46.530 回答
7

这似乎不是一般可以特别依赖的东西 - 正如你所说的 C++ ABI 非常棘手(甚至取决于编译器选项)。

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

于 2011-04-21T15:20:09.807 回答
3

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

// 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()
};

这通过添加一个条目来更改 C 的 vtable 的布局g()(感谢“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

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

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

于 2011-04-21T16:02:34.403 回答
1

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

好吧,你的直觉显然是错误的:

  • 要么在 vtable 中有一个用于覆盖器的新条目,所有后续条目都被移动,并且表增长,
  • 或者没有新条目,并且 vtable 表示没有改变。

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

无论如何:不要指望它。

于 2012-08-07T22:30:52.157 回答
0

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

在我看来,在经过适当的回归测试之后,Mark 建议使用 g++ -fdump-class-hierarchy 将是赢家


重写的东西不应该改变vtable 布局[1]。vtable 条目本身将位于 library 的数据段中,恕我直言,因此对其进行更改不会造成问题。

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

[1] 拼写出来:这假定该方法一开始是虚拟的!

于 2011-04-21T15:25:20.540 回答