Safely type punning POD-like structures in-place in C++20?

Viewed 115

There have been multiple questions asking about mmap and accessing structures in the shared memory while not invoking UB by breaking the strict aliasing rule or violating object lifetimes.

With consensus that this is not generally possible without copying the data.

More generally, over the years here, I have seen countless code snippets involving reinterpret_cast (or worse) for (de)serialization, breaking those rules. I always recommended std::memcpy while claiming that the compiler will elide those copies. Can we do better now?

I would like to clarify what is the correct approach to simply interpret a bunch of bytes as another POD type w̲i̲t̲h̲o̲u̲t̲ copying the data in C++20?

There was a proposal P0593R6 which to my knowledge got accepted into C++20.

Based on reading that, I believe the following code is safe:

template <class T>
T* pune(void* ptr) {
    // Guaranteed O(1) initialization without overwritting the data.
    auto* dest = new (ptr) std::byte[sizeof(T)];
    // There is an implicitly created T, so cast is valid.
    return reinterpret_cast<T*>(dest);
}
#include <cstring>
#include <array>
#include <fmt/core.h>

struct Foo {
    int x;
    float y;
};

auto get_buffer() {
    Foo foo{.x = 10, .y = 5.0};
    std::array<std::byte, sizeof(Foo)> buff;
    std::memcpy(buff.data(), &foo, sizeof(foo));
    return buff;
}

int main() {
    // Imagine the buffer came from a file or mmaped memory,
    // compiler does not see the memcpy above.
    auto buff = get_buffer();

    // There is alive Foo as long as buff lives and 
    // no new objects are created in there.
    auto* new_foo = pune<Foo>(buff.data());

    fmt::print("Foo::x={}\n", new_foo->x);
    fmt::print("Foo::y={}\n", new_foo->y);
}

Live demo godbolt.

Is this really safe?

Is pune really O(1)?

EDIT

Okay, how about using memmove and still rely on the compiler to do this in O(1)?

template <class T>
T* pune(void* ptr) {
    void* dest = new (ptr) std::byte[sizeof(T)];
    auto* p = reinterpret_cast<T*>(std::memmove(dest,ptr,sizeof(T)));
    return std::launder(p);
}
1 Answers

Guaranteed O(1) initialization without overwritting the data.

Not so much.

Indeed, P0593 explicitly mentions that this will not work:

Symmetrically, when the float object is created, the object has an indeterminate value, and therefore any attempt to load its value results in undefined behavior.

[basic.indent] contains the details:

When storage for an object with automatic or dynamic storage duration is obtained, the object has an indeterminate value, and if no initialization is performed for the object, that object retains an indeterminate value until that value is replaced

Placement new "obtains storage" for the object. Since no initialization is performed, it has an "indeterminate value". Attempting to read that will yield undefined behavior.

As of yet, there is no mechanism in C++ that allows you to take memory which had one object in it and read its values as another object.

However, mmap could be considered (by the implementation) to implicitly create objects in the storage it returns. As such, you could just cast such a pointer to an implicit lifetime type (not POD, which no longer exists as a category) and read the values from there.

Related