虚拟方法和vtable查找的最终说明符

时间:2015-03-04 12:09:00

标签: c++ c++11

我观察到当某个类的方法在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关键字),这就是为什么我会问这种行为是否是标准的。

3 个答案:

答案 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所允许的,并且正如反汇编中所示。