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?
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();
}