我观察到当某个类的方法在C ++ 11中被标记为final
时,vtable中没有查找来调用该方法,即使是从指针调用,至少使用由GCC。让这段代码作为例子:
class Base {
public:
Base() : retval(0) {}
virtual ~Base(){}
virtual int method() {
return retval;
}
protected:
uint32_t retval;
};
class DerivedFinal : public Base {
public:
int method() final {
return retval + 2;
}
};
int main() {
Base *bptr = new Base();
DerivedFinal *df = static_cast<DerivedFinal *>(bptr);
return df->method();
}
请注意,代码使用这样的返回值,以使汇编代码易于阅读。
主要的装配看起来像这样:
<+0>: push %rbp
<+1>: mov %rsp,%rbp
<+4>: push %rbx
<+5>: sub $0x18,%rsp
<+9>: mov $0x10,%edi
<+14>: callq 0x400750 <_Znwm@plt>
<+19>: mov %rax,%rbx
<+22>: mov %rbx,%rdi
<+25>: callq 0x400900 <_ZN4BaseC2Ev>
<+30>: mov %rbx,-0x18(%rbp)
<+34>: mov -0x18(%rbp),%rax
<+38>: mov %rax,-0x20(%rbp)
<+42>: mov -0x20(%rbp),%rax
<+46>: mov %rax,%rdi
<+49>: callq 0x400986 <_ZN12DerivedFinal6methodEv> // This is the method call
<+54>: add $0x18,%rsp
<+58>: pop %rbx
<+59>: pop %rbp
<+60>: retq
可以看出,在没有任何vtable查找的情况下调用该方法(如果该方法未标记为final,则不会发生这种情况)。即使存在从DerivedFinal
继承的类,代码也会以相同的方式运行。我的问题是......这是标准行为吗?
编辑:让我们以非定义行为的方式重写代码,以明确显示当方法最终时如何跳过vtable,并在它被查找时查找不是:
class Base {
public:
Base() : retval(0) {}
virtual ~Base(){}
virtual int method() {
return retval;
}
protected:
uint32_t retval;
};
class DerivedFinal : public Base {
public:
int method() final {
return retval + 2;
}
};
class DerivedNotFinal : public Base {
public:
int method() {
return retval + 3;
}
};
int main() {
DerivedFinal *df = new DerivedFinal();
DerivedNotFinal *dnf = new DerivedNotFinal();
int res_final = df->method();
int res_not_final = dnf->method();
return 0;
}
汇编转储:
<+0>: push %rbp
<+1>: mov %rsp,%rbp
<+4>: push %rbx
<+5>: sub $0x28,%rsp
<+9>: mov $0x10,%edi
<+14>: callq 0x4007b0 <_Znwm@plt>
<+19>: mov %rax,%rbx
<+22>: movq $0x0,(%rbx)
<+29>: movl $0x0,0x8(%rbx)
<+36>: mov %rbx,%rdi
<+39>: callq 0x400a5c <_ZN12DerivedFinalC2Ev> // First ctor...
<+44>: mov %rbx,-0x18(%rbp)
<+48>: mov $0x10,%edi
<+53>: callq 0x4007b0 <_Znwm@plt>
<+58>: mov %rax,%rbx
<+61>: movq $0x0,(%rbx)
<+68>: movl $0x0,0x8(%rbx)
<+75>: mov %rbx,%rdi
<+78>: callq 0x400a82 <_ZN15DerivedNotFinalC2Ev> // Second ctor...
<+83>: mov %rbx,-0x20(%rbp)
<+87>: mov -0x18(%rbp),%rax
<+91>: mov %rax,%rdi
<+94>: callq 0x400a34 <_ZN12DerivedFinal6methodEv> // Call to DerivedFinal::method directly
<+99>: mov %eax,-0x24(%rbp) // Save result in stack
<+102>: mov -0x20(%rbp),%rax
<+106>: mov (%rax),%rax
<+109>: add $0x10,%rax
<+113>: mov (%rax),%rax
<+116>: mov -0x20(%rbp),%rdx
<+120>: mov %rdx,%rdi
<+123>: callq *%rax // Call to DerivedNotFinal::method via vtable (indirect call)
<+125>: mov %eax,-0x28(%rbp) // Save result in stack
<+128>: mov $0x0,%eax
<+133>: add $0x28,%rsp
<+137>: pop %rbx
<+138>: pop %rbp
<+139>: retq
通过这个例子,行为也很清楚。对DerivedFinal ::方法的调用不需要任何vtable查找,而对DerivedNotFinal ::方法的调用需要间接。在我看来,在某些性能关键型应用程序中需要这种行为(带有final关键字),这就是为什么我会问这种行为是否是标准的。
答案 0 :(得分:5)
您的代码通过static_cast
指向DerivedFinal
DerivedFinal *
final
之内的指针来调用未定义的行为。
编译器有权做任何事情,包括让你的猫怀孕或召唤鼻子恶魔。
在一个更好的示例中,标记为{{1}}的方法无法在另一个派生类中被覆盖,因此编译器可能 - 以及一个优秀的编译器应该 - 对该调用进行虚拟化。
答案 1 :(得分:1)
标准只是指定了行为。在没有未定义的行为的情况下,程序必须表现为,好像基于对象的动态类型查找虚函数;但是,如果编译器可以确定正确的覆盖,则标准中没有任何内容指定它必须实际执行运行时查找。
由于您有一个指向DerivedFinal
的指针,并且该类的覆盖为final
,因此编译器知道必须选择哪个覆盖,因此可以生成对该虚拟调用的非虚拟调用。它不必考虑您可能使用static_cast
对该类型撒谎的可能性;导致未定义的行为,因此在这种情况下允许它做任何事情。
答案 2 :(得分:0)
该标准允许任何机器代码重现源代码所需的效果。
如果此机器代码执行此操作,即如果它调用 Base::method
,那么它就可以了。
否则,不是。
请注意,此代码具有正式的未定义行为,方法是将Base
对象视为类DerivedFinal
的对象。
因此它可以做任何事情并且仍然符合要求。
但我猜测可以创建表现出相同现象的表现良好的代码,使用特定的编译器和选项。此外,微软为其ATL库使用了这样的演员,以便神奇地使某些成员函数无法访问(防止无意中使用它们)。因此,使用Visual C ++编译器显然已经很好地定义了它,并且很可能也使用了g ++ - 但是我们进入了特定于编译器的领域。
如果你想知道为什么,编译器可以在这里省略查找,因为成员函数在静态已知类型中是final
,它不可能是被某些派生类覆盖。因此可以直接调用静态已知类型的成员函数实现。因此,我怀疑上面的机器代码调用DerivedFinal::method
,因为它是正式的UB所允许的,并且正如反汇编中所示。