c++type-punning

how do I use type punning, without having to use memcpy


So, I hav a class that has a variable alignas(void*) bool failed inside. That failed variable contains an address.

    auto getFailed() {
        unsigned long long adress;
        memcpy(&adress, &(this->failed), sizeof(void*));
        return ((BaseA*)(void*)(adress))->failed;
    }

The above code works well, but I think its kind of ugly to do that memcpy thing

So I tried the following:

    auto getFailed() {
        return ((BaseA*)&(this->failed))->failed;
    }

In theorie this should work just fine, but it does not. I am not really sure why, and how to fix it.


Solution

  • You cannot really type-pun a bool into an unsigned long long via memcpy (unless they have the same size). Putting alignas(void*) on a bool doesn't solve this, because you're reading bytes that are past its object representation. It is memory-safe to do because of padding in the surrounding struct, but it's still undefined behavior in C++.

    See also: Why does std::memcpy (as an alternative to type-punning) not cause undefined behaviour? tl; dr: the only thing you can do with std::memcpy is copy values between two objects of the same type. Anything beyond that is a compiler extension.

    On a side note, it would be much better to use std::uintptr_t instead of unsigned long long, because it is guaranteed to have the same size as void*.

    Solution with std::bit_cast

    Since C++20, there is a function that does this memcpy step for you:

    auto getFailed() {
        return std::bit_cast<std::uintptr_t>(this->failed);
    }
    

    However, you cannot use it, because it requires sizeof(this->failed) == sizeof(std::uintptr_t). You would have to put your bool into a wrapper struct with the same size as unsigned long long instead of padding it via alignas:

    struct {
        // important: Don't use alignas directly, because void* could have no alignment
        //            requirements despite being 8 bytes in size.
        //            In that case, we would have no padding.
        alignas(sizeof(void*)) bool value;
    } failed;
    

    However, this is still technically UB, because bit-casting padding bits is UB in most cases. You must bit-cast bits which are part of the value representation of an object, so we must add manual paddding:

    alignas(void*) struct {
        bool value;
        char padding[sizeof(void*) - sizeof(bool)];
    } failed;
    

    Solution with reinterpret_cast

    auto getFailed() {
        // your code, but C-style cast converted to reinterpret_cast
        return reinterpret_cast<BaseA*>(&this->failed)->failed;
    }
    

    Don't even consider this, it's undefined behavior. It would be a violation of strict aliasing to read a std::uintptr_t through a pointer that is actually pointing to bool. These two types cannot alias each other.