是否可以通过在基 class 中添加新的虚函数来破坏代码?
Is it possible to break code by adding a new virtual function in the base class?
是否可以通过简单地将新的虚函数添加到基 class 来改变观察到的程序行为?我的意思是必须对代码进行其他更改。
以下程序打印 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()
的调用绑定从静态变为动态。
二进制不兼容。
如果您有一个外部可加载模块(即 DLL),那么它使用基的旧定义 class 您将遇到问题。或者,如果加载程序具有旧定义而 DLL 具有新定义,则问题相同。如果您出于某种原因使用原始二进制复制(不是任何类型的序列化)将对象保存在文件中,这也是一个问题。
这与虚函数的 C++ 规范无关,而是大多数编译器如何实现它们。
一般来说,如果 class 的 "interface" 发生变化(基数 class 或未发生变化),那么您应该重新编译使用 class 的所有内容。
当 API 以向后不兼容的方式更改时,不再保证依赖于 API 早期版本的代码可以正常工作。
所有派生 类 都依赖于其基础 类 的 API。
添加虚函数是向后不兼容的更改。 答案展示了 API 破损如何表现出来的一个很好的例子。
因此,是的,添加虚函数可能会破坏程序,除非依赖部分被修复以与新的 API 一起工作。这意味着无论何时添加虚函数,都应该检查所有派生的 类,并确保它们各自的 API 的含义没有被添加改变。
#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();
}
如果没有虚函数,基础 class b.foo()
是对 B::foo()
:
的非虚调用
$ g++ virt.cc
$ ./a.out
在基础 class 中使用虚拟调用 C::foo()
:
$ g++ virt.cc -DADD_TO_BASE
$ ./a.out
Aborted (core dumped)
由于二进制不兼容,您还可能会遇到令人讨厌的未定义行为,因为将虚函数添加到非多态基 class 会改变其大小和布局(需要重新编译所有其他使用它的翻译单元).
向已经多态的基础添加一个新的虚函数 class 改变 vtable 的布局,要么在末尾添加一个新条目,要么改变其他函数的位置,如果添加在中间(和即使添加在基本 vtable 的末尾,对于任何派生的 classes 来说,它也位于 vtable 的中间,这些派生 classes 添加了新的虚函数)。这意味着使用 vtable 的已编译代码可能最终会调用错误的函数,因为它使用了 vtable 中的错误槽。
二进制不兼容问题可以通过重新编译所有相关代码来修复,但是行为上的静默更改(如顶部示例)不能简单地通过重新编译来修复。
是否可以通过简单地将新的虚函数添加到基 class 来改变观察到的程序行为?我的意思是必须对代码进行其他更改。
以下程序打印 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()
的调用绑定从静态变为动态。
二进制不兼容。
如果您有一个外部可加载模块(即 DLL),那么它使用基的旧定义 class 您将遇到问题。或者,如果加载程序具有旧定义而 DLL 具有新定义,则问题相同。如果您出于某种原因使用原始二进制复制(不是任何类型的序列化)将对象保存在文件中,这也是一个问题。
这与虚函数的 C++ 规范无关,而是大多数编译器如何实现它们。
一般来说,如果 class 的 "interface" 发生变化(基数 class 或未发生变化),那么您应该重新编译使用 class 的所有内容。
当 API 以向后不兼容的方式更改时,不再保证依赖于 API 早期版本的代码可以正常工作。
所有派生 类 都依赖于其基础 类 的 API。
添加虚函数是向后不兼容的更改。
因此,是的,添加虚函数可能会破坏程序,除非依赖部分被修复以与新的 API 一起工作。这意味着无论何时添加虚函数,都应该检查所有派生的 类,并确保它们各自的 API 的含义没有被添加改变。
#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();
}
如果没有虚函数,基础 class b.foo()
是对 B::foo()
:
$ g++ virt.cc
$ ./a.out
在基础 class 中使用虚拟调用 C::foo()
:
$ g++ virt.cc -DADD_TO_BASE
$ ./a.out
Aborted (core dumped)
由于二进制不兼容,您还可能会遇到令人讨厌的未定义行为,因为将虚函数添加到非多态基 class 会改变其大小和布局(需要重新编译所有其他使用它的翻译单元).
向已经多态的基础添加一个新的虚函数 class 改变 vtable 的布局,要么在末尾添加一个新条目,要么改变其他函数的位置,如果添加在中间(和即使添加在基本 vtable 的末尾,对于任何派生的 classes 来说,它也位于 vtable 的中间,这些派生 classes 添加了新的虚函数)。这意味着使用 vtable 的已编译代码可能最终会调用错误的函数,因为它使用了 vtable 中的错误槽。
二进制不兼容问题可以通过重新编译所有相关代码来修复,但是行为上的静默更改(如顶部示例)不能简单地通过重新编译来修复。