【问题标题】:In C++, does overriding an existing virtual function break ABI?在 C++ 中,覆盖现有的虚函数会破坏 ABI 吗?
【发布时间】:2011-04-21 15:13:02
【问题描述】:

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

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

【问题讨论】:

  • 如果你在做 ABI 为什么不使用纯 C 作为接口?
  • 因为它是一个 C++ 库,旨在以惯用的 C++ 方式使用。你认为 QT 应该只在 C 中提供他们的 API 吗? :P

标签: c++ backwards-compatibility subclassing virtual-functions abi


【解决方案1】:

可能会。

关于偏移量,你错了。 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

【讨论】:

  • 这就是为什么虚拟方法应该始终是私有的。
  • @grumm143:但这还不够。内联仍然可以发挥作用。
  • 怎么样?虚拟方法只能通过公共的非虚拟接口调用,它不依赖于类的确切类型。虚拟调用只能在库本身内内联,但这显然不是问题。
  • @grumm143:啊,那你的意思是公共方法定义没有公开(防止内联)。
  • Derived 类构造函数将用 Derived 覆盖替换该偏移处的函数指针”构造函数不会与 vtable 混淆。事实上,vtable 是常量(但可能需要在加载时由动态链接器修复)。 永远不会动态修改的 vtable。
【解决方案2】:

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

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

【讨论】:

  • 如果编译器能够对多个函数调用进行去虚拟化,则可以:/
【解决方案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()
};

这通过添加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

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

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

【讨论】:

  • 感谢您分享此信息。在我看来Mark's 建议使用g++ -fdump-class-hierarchy 将是这里的赢家,就在进行适当的回归测试之后;)
【解决方案4】:

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

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

  • 要么在 vtable 中为覆盖程序创建一个新条目,所有后续条目都被移动,并且表格增长,
  • 或者没有新条目,并且vtable表示没有改变。

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

无论如何:不要指望它。

【讨论】:

    【解决方案5】:

    注意:请参阅In C++, does overriding an existing virtual function break ABI? 了解此逻辑不成立的情况;

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


    覆盖的东西不应该改变 vtable 布局[1]。 vtable 条目本身将 在库的数据段中,恕我直言,因此对 it 的更改不会造成问题。

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

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

    【讨论】:

    • "_Overriding things should not change vtable layout" 错误。这取决于。
    • @curiousguy 我想我已经说得很清楚了。我还链接到相关资源以根据实际情况做出判断。因为……这取决于实际情况。
    • "我想我已经说得很清楚了。" 我不清楚,抱歉。你是说,对于单继承,添加覆盖器不会改变 vtable 的布局?
    • @curiousguy 是的,这是我的理解。如果这不正确,您为什么不提供描述此类情况的答案,以便我们对其进行投票?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-06-17
    • 1970-01-01
    • 1970-01-01
    • 2020-08-21
    • 2010-10-03
    • 2010-11-27
    相关资源
    最近更新 更多