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

Sam*_*Lou 3 c++ c++11

我观察到当某个类的方法被标记为final在C++ 11中时,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();
}
Run Code Online (Sandbox Code Playgroud)

请注意,代码使用这样的返回值来使汇编代码易于阅读.

主要的装配看起来像这样:

<+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
Run Code Online (Sandbox Code Playgroud)

可以看出,在没有任何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;
}
Run Code Online (Sandbox Code Playgroud)

和汇编转储:

<+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
Run Code Online (Sandbox Code Playgroud)

通过这个例子,行为也很清楚.对DerivedFinal ::方法的调用不需要任何vtable查找,而对DerivedNotFinal ::方法的调用需要间接.在我看来,在某些性能关键型应用程序中需要这种行为(带有final关键字),这就是为什么我会问这种行为是否是标准的.

T.C*_*.C. 5

您的代码通过static_cast指向不属于DerivedFinal某个东西的指针来调用未定义的行为DerivedFinal *.

编译器有权做任何事情,包括让你的猫怀孕或召唤鼻子恶魔.

在一个更好的示例中,标记的方法final不能在另一个派生类中被覆盖,因此编译器可能 - 以及一个优秀的编译器应该 - 对调用进行虚拟化.