Base to Derived To another Base cast in constructor segfault — invalid code or compiler bug?

Viewed 89

This code gives me a segfault sometimes or invalid result on GCC/Clang. However, it works fine on MSVC & ICC. I'm not sure if my code is invalid from the eyes of the standard or if it is a compiler bug in GCC/Clang.

#include <cstdio>

struct A {
    virtual void func() {}
    void* junk = nullptr;
};

template<typename T>
struct B {
    B() {
        T* cp = static_cast<T*>(this);
        A* a = cp;
        printf("%p\n", a);
    }
};

struct C : virtual A, B<C>
{
    C() : A(), B<C>() {}
};

int main() {
    C c;

    A* ptr = &c;
    printf("%p\n", ptr);

    return 0;
}

One such incorrect result on Godbolt (Clang). Both pointers are a result of cast to same base A and as such should give same address:

ASM generation compiler returned: 0
Execution build compiler returned: 0
Program returned: 0
0x7fff9fa82608
0x7fff9fa82510

I have debugged it to understand the root cause behind it. The static_cast<T*>(this) is fine. But the pointer conversion from T* -> A* requires the vtable to get the offset of the base class since A it is a virtual base class. You can see this if you compile this function on godbolt:

A& func(C& c) {
    return c;
}

Generated code:

func(C&):
mov     rax, rdi                     % rax = address of input C object
mov     rcx, qword ptr [rdi]         % rcx = load vtable of C
add     rax, qword ptr [rcx - 24]    % rax += offset of virtual base class A
ret

However, in the constructor of B, the vtable of the *this object is not set to the vtable of class C. Thus when it gets the offset of virtual base class A from the vtable, it reads some junk value and calculates an invalid address for a.

I'm well aware, I could also modify the constructor of B to: B(A* a) {} and that would work. However, this option is not possible for me as this example is a reduced version of a larger system.

I added another virtual class to C and it still works correctly in ICC and MSVC: Compiler explorer

2 Answers

The code has Undefined Behavior, so it is not surprising that you get different results with different compilers. The UB comes from having to access data that is part of the C class before the C object has been constructed. That access is necessary during the pointer conversion required by A* a = cp;, because the location of the A subobject relative to the C subobject can change.

As an example of how it can change, consider this larger class hierarchy:

struct D: virtual A {
};

struct E: D, C {
};

This would result in a class that looks something like:

    A
   / \
  /   B
 D    |
  \   C
   \ /
    E

This problem started with the cast from the base with T* cp = static_cast<T*>(this);. this is a B object (nothing more, nothing less), but you're telling the compiler that it is really a C object, which it isn't (yet). It won't become a C object until after the constructor of the B subobject finishes running and the body of the C constructor starts executing.

I think this is covered by N4860 [class.cdtor]/1:

For an object with a non-trivial constructor, referring to any non-static member or base class of the object before the constructor begins execution results in undefined behavior.

In this code, A* a = cp; refers to the base class of the object c before the constructor C() has been entered for that object.

Related clauses (but not directly applicable), [class.cdtor]/3 and [basic.life]/6.

Related