【问题标题】:Is it possible to break code by adding a new virtual function in the base class?是否可以通过在基类中添加新的虚函数来破坏代码?
【发布时间】:2016-07-29 11:19:51
【问题描述】:

是否可以通过简单地将新的虚函数添加到基类来改变观察到的程序行为?我的意思是不必对代码进行任何其他更改。

【问题讨论】:

标签: c++ polymorphism virtual-functions


【解决方案1】:

以下程序打印OK。取消注释B 中的虚函数,它将开始打印CRASH!

#include <iostream>

struct B
{
    //virtual void bar() {}
};

struct D : B
{
    void foo() { bar(); }
    void bar() { std::cout << "OK" << std::endl; }
};

struct DD : D
{
    void bar() { std::cout << "CRASH!" << std::endl; }
};

int main()
{
    DD d;
    d.foo();
    return 0;
}

问题是在引入虚函数B::bar() 后,D::foo() 中对bar() 的调用绑定从静态变为动态。

【讨论】:

  • 但这首先是virtual函数的目的......这里没有什么令人惊讶的:-)。只要您在派生类中具有相同的函数签名,但virtual 与基类之一一样,它会自动被覆盖。签名略有不同的错误部分是导致override关键字的原因
  • @WhiZTiM 这个问答是插图requested elsewhere
  • @WhiZTiM:不,这并不奇怪,但它确实提供了一个例子来证明在基类中引入虚函数可以破坏以前的工作代码(这是OP提出的问题)。
  • 我明白了。好点...赞成。 :-)。但是@Leon,我认为您还应该修改答案以指出修改基类的二进制不兼容性。
【解决方案2】:

二进制不兼容。

如果您有一个外部可加载模块(即 DLL),那么它使用基类的旧定义,您将遇到问题。或者,如果加载程序有旧定义而 DLL 有新定义,这也是同样的问题。如果您出于某种原因使用原​​始二进制复制(不是任何类型的序列化)将对象保存在文件中,这也是一个问题。

这与虚函数的 C++ 规范无关,而是大多数编译器如何实现它们。

一般来说,如果类的“接口”发生变化(基类与否),那么您应该重新编译使用该类的所有内容。

【讨论】:

  • 比这更糟糕,请参阅 Leon 的答案 - 它不需要 C++ 标准之外的任何内容。
【解决方案3】:
#include <stdlib.h>

struct A {
#if ADD_TO_BASE
  virtual void foo() { }
#endif
};

struct B : A {
  void foo() { }
};

struct C : B {
  void foo() { abort(); }
};

int main() {
  C c;
  B& b = c;
  b.foo();
}

没有虚函数,基类b.foo() 是对B::foo() 的非虚调用:

$ g++ virt.cc
$ ./a.out

在基类中使用 virtual 是对 C::foo() 的虚拟调用:

$ g++ virt.cc -DADD_TO_BASE
$ ./a.out
Aborted (core dumped)

由于二进制不兼容,您还可能得到令人讨厌的未定义行为,因为向非多态基类添加虚函数会改变其大小和布局(需要重新编译所有其他使用它的翻译单元)。

向已经多态的基类添加新的虚函数会改变 vtable 的布局,要么在末尾添加新条目,要么在中间添加时更改其他函数的位置(即使添加在末尾基础 vtable 的中间,对于任何添加新虚函数的派生类,它位于 vtable 的中间)。这意味着使用 vtable 的已编译代码最终可能会调用错误的函数,因为它使用了 vtable 中的错误槽。

可以通过重新编译所有相关代码来修复二进制不兼容问题,但无法简单地通过重新编译来修复像顶部示例这样的无声行为变化。

【讨论】:

    【解决方案4】:

    当 API 以向后不兼容的方式更改时,依赖于 API 早期版本的代码不再保证可以正常工作。

    所有派生类都依赖于其基类的 API。

    添加虚函数是向后不兼容的更改。 Leon's 答案展示了 API 损坏如何表现出来的一个很好的示例。

    因此,是的,添加虚函数可能会破坏程序,除非相关部分已修复以使用新 API。这意味着无论何时添加一个虚函数,都应该检查所有派生类,并确保它们各自的 API 的含义没有因添加而改变。

    【讨论】:

    • 您断言添加虚函数是一种向后不兼容的更改,但您没有提供该断言的任何证据。鉴于该问题可以改写为“向基类添加虚函数是一种向后不兼容的更改”,因此只说“是”并不是很有帮助。
    • @MartinBonner 改变会改变问题的含义(如果你愿意的话,这是一个向后不兼容的改变)尽管是微妙的。我同意,我的主张应该附有一个例子。 Leon 已经发布了一个很好的例子,我认为没有必要重复它。但是,我将参考它来支持我的断言。
    猜你喜欢
    • 1970-01-01
    • 2011-03-23
    • 2023-03-08
    • 1970-01-01
    • 2019-06-17
    • 2012-03-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多