c++deserializationtype-punning

Creating a trivially constructible object from a char const* without default constructing?


Given a trivially constructible struct with alignment > 1, how can I create that struct with data from a buffer without calling the default constructor?

enum class FoodType {
    Fruit, Veggie, Meat;
};

struct Banana {
    int64_t hp;
    int64_t deliciousness;
    FoodType type;
};

Specifically, I don't want to default construct Banana because I don't want to default init type. I also cant copy a reinterpret_cast (Banana b = *reinterpret_cast<Banana*>(buffer)) because the alignment of Banana isn't 1.

Banana Read(char const* buffer) {
    return WHAT_DO_I_DO(buffer);
}

Solution

  • You can solve this in C++17 using std::memcpy as follows:

    template <typename T>
      // remove std::is_implicit_lifetime_v prior to C++23
      requires (std::is_trivially_copyable_v<T> && std::is_implicit_lifetime_v<T>)
    T Read(char const* buffer) {
        alignas(T) unsigned char bytes[sizeof(T)];
        std::memcpy(bytes, buffer, sizeof(T));
        return *std::launder(reinterpret_cast<T*>(bytes));
    }
    

    This works because std::memcpy implicitly creates a T object in bytes, which can be accessed via reinterpret_cast. However, this solution isn't constexpr because of std::memcpy and reinterpret_cast. This can be solved with C++20's std::bit_cast:

    template <typename T>
      requires std::is_trivially_copyable_v<T>
    constexpr T Read(char const* buffer) {
        char bytes[sizeof(T)];
        std::copy_n(buffer, sizeof(T), bytes);
        return std::bit_cast<T>(bytes);
    }
    
    // Example usage (assuming little-endian, 32-bit int)
    constexpr char bytes[] = {1, 2, 3, 4};
    static_assert(Read<int>(bytes) == 0x04030201);
    

    Note that while the implementation looks relatively expensive, it actually gets optimized down to just (https://godbolt.org/z/s78dGo7dM):

    T Read<int>(char const*):
            mov     eax, dword ptr [rdi]
            ret
    

    Another solution based on C++23's std::start_lifetime_as:

    template <typename T>
      requires std::is_implicit_lifetime_v<T>
    constexpr T Read(char const* buffer) {
        // implicit call to copy constructor here because we return T by value
        return std::start_lifetime_as<T>(buffer);
    }
    

    However, this solution is purely theoretical because no compiler implements std::start_lifetime_as yet.