第一个示例:使用非虚拟调用在Base构造函数中调用纯虚拟函数foo()。这就解释了为什么代码会正常执行,也就是说,它不会像第二个示例那样中止。
#include <iostream>
struct Base {
Base() { foo(); }
virtual void foo() = 0;
};
void Base::foo() { std::cout << "Base::foo()\n"; }
struct Derived : Base { void foo() { std::cout << "Derived::foo()\n"; } };
int main()
{
Derived d;
}第二个示例:这里,纯虚函数foo()也在Base ctor中调用,但是使用虚拟调用,代码用R6025 - pure virtual function call中止。
#include <iostream>
struct Base {
Base() { call_foo(); }
virtual void foo() = 0;
void call_foo() { foo(); }
};
void Base::foo() { std::cout << "Base::foo()\n"; }
struct Derived : Base { void foo() { std::cout << "Derived::foo()\n"; } };
int main()
{
Derived d;
}我知道在第10.4/6节中,从构造函数或析构函数调用的纯虚函数被认为是未定义的行为。但我很想知道,对于这两个片段中对foo()的不同调用,有什么合理的解释呢?
发布于 2014-05-15 15:44:29
正如其他人所说,这是一种未定义的行为。然而,(可能)发生的是,在第一种情况下(构造函数中的调用),编译器知道对象的动态类型(因为它始终是正在构造的类型),因此生成调用与调用任何非虚拟函数完全一样。(如果你没有给出一个定义,我想链接器会抱怨的。)在第二种情况下,从另一个函数中调用该函数。此时,编译器无法知道对象在运行时将具有的动态类型;函数可以在完全构造的对象上调用,可能是派生类型,甚至在这个源文件中都不存在。由于编译器不能静态地确定对象在运行时将具有的动态类型,所以它必须通过虚拟函数表(或者它用来解析动态分派的任何方法,但是VS,和我认识的其他人一样,使用vptr到vtable)来生成一个调用。因为以这种方式调用纯虚拟函数是未定义的行为,所以编译器在vtable中放置一个指向错误处理例程的指针。
注意,在这种情况下,编译器可以看到Base构造函数中的调用将以一种导致未定义行为的方式解析,并生成可能触发错误的代码。或者..。由于Base的所有虚拟函数都是纯虚拟的,编译器可能知道在构造过程中不能调用该函数,甚至不用为Base创建一个单独的vtable,可能会在调用Base的构造函数之前初始化vptr以指向Derived的vtable。你不能指望任何这样的案子。
发布于 2014-05-15 14:40:26
在第一种情况下,直接从构造函数调用函数,动态类型在编译时是已知的,因此不需要虚拟分派。编译器可以生成对Base::foo的直接调用。
在第二种情况下,从从Base派生的任何类型上可以随时调用的另一个函数调用它,在编译时不知道动态类型,因此虚拟调度是必要的。
正如你所说,这是未定义的行为;原则上,任何事情都可能发生。我希望在第一种情况下会出现编译器警告(GCC给出了一个警告);但是在第二种情况下,错误只能在运行时检测到(如果有的话)。
发布于 2014-05-15 14:43:19
嗯,未定义的行为意味着任何事情都可能发生;包括可能令人惊讶的事件,比如看起来有效或抛出异常。
现在,鉴于VS选择在构建或销毁期间发出对纯虚拟的调用时中止;我怀疑第一个行为(调用Base::foo)实际上是一个bug (根据它们的规范)。对于去虚拟化函数调用的代码来说,如果调用的方法是纯的,那么它就会忽略无法编译的特殊情况。
https://stackoverflow.com/questions/23681392
复制相似问题