Search code examples
c++c++20type-punning

Type-pun uint64_t as two uint32_t in C++20


This code to read a uint64_t as two uint32_t is UB due to the strict aliasing rule:

uint64_t v;
uint32_t lower = reinterpret_cast<uint32_t*>(&v)[0];
uint32_t upper = reinterpret_cast<uint32_t*>(&v)[1];

Likewise, this code to write the upper and lower part of an uint64_t is UB due to the same reason:

uint64_t v;
uint32_t* lower = reinterpret_cast<uint32_t*>(&v);
uint32_t* upper = reinterpret_cast<uint32_t*>(&v) + 1;

*lower = 1;
*upper = 1;

How can one write this code in a safe and clean way in modern C++20, potentially using std::bit_cast?


Solution

  • Using std::bit_cast:

    Try it online!

    #include <bit>
    #include <array>
    #include <cstdint>
    #include <iostream>
    
    int main() {
        uint64_t x = 0x12345678'87654321ULL;
        // Convert one u64 -> two u32
        auto v = std::bit_cast<std::array<uint32_t, 2>>(x);
        std::cout << std::hex << v[0] << " " << v[1] << std::endl;
        // Convert two u32 -> one u64
        auto y = std::bit_cast<uint64_t>(v);
        std::cout << std::hex << y << std::endl;
    }
    

    Output:

    87654321 12345678
    1234567887654321
    

    std::bit_cast is available only in C++20. Prior to C++20 you can manually implement std::bit_cast through std::memcpy, with one exception that such implementation is not constexpr like C++20 variant:

    template <class To, class From>
    inline To bit_cast(From const & src) noexcept {
        //return std::bit_cast<To>(src);
        static_assert(std::is_trivially_constructible_v<To>,
            "Destination type should be trivially constructible");
        To dst;
        std::memcpy(&dst, &src, sizeof(To));
        return dst;
    }
    

    For this specific case of integers quite optimal would be just to do bit shift/or arithmetics to convert one u64 to two u32 and back again. std::bit_cast is more generic, supporting any trivially constructible type, although std::bit_cast solution should be same optimal as bit arithmetics on modern compilers with high level of optimization.

    One extra profit of bit arithmetics is that it handles correctly endianess, it is endianess independent, unlike std::bit_cast.

    Try it online!

    #include <cstdint>
    #include <iostream>
    
    int main() {
        uint64_t x = 0x12345678'87654321ULL;
        // Convert one u64 -> two u32
        uint32_t lo = uint32_t(x), hi = uint32_t(x >> 32);
        std::cout << std::hex << lo << " " << hi << std::endl;
        // Convert two u32 -> one u64
        uint64_t y = (uint64_t(hi) << 32) | lo;
        std::cout << std::hex << y << std::endl;
    }
    

    Output:

    87654321 12345678
    123456788765432
    

    Notice! As @Jarod42 points out, solution with bit shifting is not equivalent to memcpy/bit_cast solution, their equivalence depends on endianess. On little endian CPU memcpy/bit_cast gives least significant half (lo) as array element v[0] and most significant (hi) in v[1], while on big endian least significant (lo) goes to v[1] and most significant goes to v[0]. While bit-shifting solution is endianess independent, and on all systems gives most significant half (hi) as uint32_t(num_64 >> 32) and least significant half (lo) as uint32_t(num_64).