c++inheritancevirtual-functionsdynamic-dispatchvirtual-table

Why a member function being virtual affects the compiled code of a TU where no virtual dispatch is actually required?


In a TU like this

#include "Foo.hpp"
int main() {
    // stuff
    Foo* foo{new Foo{}};
    foo->foo();
    // stuff
}

where Foo.hpp contains

#pragma once
struct Foo {
    virtual void foo(); // implmented somewhere
};

no call to anything other than Foo::foo can happen, right? foo is virtual and nor it nor the class are final, so yes, in another TU there could be objects of derived classes that override foo, and so on, but... as far as this TU is concerned I think it's pretty clear that foo->foo() calls Foo::foo(). I don't see how it could be otherwise.

Then why is the generated assembly like this?

main:                                   # @main
        push    rax
        mov     edi, 8
        call    operator new(unsigned long)@PLT
        mov     rcx, qword ptr [rip + vtable for Foo@GOTPCREL]
        add     rcx, 16
        mov     qword ptr [rax], rcx
        mov     rdi, rax
        call    Foo::foo()@PLT
        xor     eax, eax
        pop     rcx
        ret

I don't really understad it in detail, but I clearly read vtable. Why is it even there?

I'd have expected the assembly of the TU above to be the same as the one I get if I remove the virtual keyword:

main:                                   # @main
        push    rax
        mov     edi, 1
        call    operator new(unsigned long)@PLT
        mov     rdi, rax
        call    Foo::foo()@PLT
        xor     eax, eax
        pop     rcx
        ret

(Here's the example on CE.)

From this other answer I read that

A* a = new B;
a->func();

In this case the compiler can determine that a points to a B object and, thus, call the correct version of func() without dynamic dispatch. […] Of course, whether compilers do the corresponding analysis depends on its respective implementation.

Is the answer simply that Clang does not make the analysis that would allow to deduce that no runtime dispatch is needed?

Or am I missing something? Maybe I'm simply totally misunderstanding the assembly?


Solution

  • This line

    mov     rcx, qword ptr [rip + vtable for Foo@GOTPCREL]
    

    initialises the vtable pointer.

    The vtable mechanism is not used to call Foo::foo, however each object that has a vtable needs to have its vtable pointer initialised. If you don't initialise the vtable pointer of the Foo object, Foo::foo() may break (for example, it can try and dynamic_cast this).

    If you give Foo::foo an inlinable body, the compiler may (or may not) optimise away the entire Foo object, depending on what exactly is in the body. However it is unlikely that the compiler will allow an object with uninitialised vtable pointer to exist. It is just too much trouble and not worth the effort to handle this fringe case.