Search code examples
c++gccvisual-c++undefined-behavior

Is it legal to `reinterperet_cast` to the child of a non-virtual object?


This is related to the question about type erasure here: boost te memory access failure with visual c++. Feel free to edit the title if you think you can phrase this a better way.

In the type erasure library, a trick is performed which is essentially akin to the following:

struct implementer {
  void func(){};
};

struct base {
  virtual void f() const = 0;
};

struct poly : public implementer, public base {
    void f() const {}
};

template <typename T>
void call(const T& v) {
    const auto& b= reinterpret_cast<const base &>(v);
    b.f();
}

int main() {
    poly p;
    implementer &i = p;
    call(i);
}

The key here being that because you are passing the implementer to the call function, reinterperet_cast must be used to get the poly part out and do the retrieval of the erased function. This compiles and runs (correctly getting the base vtable) in gcc and clang.

https://godbolt.org/z/os9W71av6

It also compiles in MSVC, but when I run it, the reinterperet_cast breaks the vtable for the value b leading to a read access violation:

Exception thrown: read access violation. b was 0xFFFFFFFFFFFFFFFF.

So the question then is is this type of cast legal in c++, or is it undefined behaviour?


Solution

  • Why this seems to "work" on GCC/Clang on Linux:

    implementer is subject to empty base class optimization and is placed in the first zero bytes of poly. The next 8 bytes store the vtable pointer and all zero members of base.

    This means that p, static_cast<implementer&>(p) (i) and static_cast<base&>(p) all have the same address.

    You still have UB because the reinterpret_cast doesn't point to a base object, but that can be fixed by a std::launder:

    const auto& b = *std::launder(&reinterpret_cast<const base &>(v));
    b.f();  // No longer UB
    

    Why this doesn't work on MSVC/Clang with the Microsoft ABI:

    Since poly isn't standard layout, the members are allowed to be rearranged. Crucially, implementer being stored in zero bytes at the beginning is not guaranteed. The Microsoft ABI stores 8 bytes for the vtable pointer / base and then zero bytes for base.

    This means &i != &p (it is 8 bytes greater, &i == reinterpret_cast<char*>(p) + sizeof(void*)).

    So that means that your reinterpret_cast is at the wrong address. You would have to adjust the pointer again on Windows:

    const auto& b = *std::launder(reinterpret_cast<const base *>(&v) - 1);
    b.f();
    

    The amount you need to adjust would obviously be different depending on the exact layout of the class.


    What you are doing is essentially a sidecast. If you make implementer virtual you can do this with a dynamic_cast:

    struct implementer {
        void func() {}
        virtual ~implementer() = default;
    };
    
    // ...
    
    template <typename T>
    void call(const T& v) {
        const auto& b = dynamic_cast<const base &>(v);
        b.f();
    }