Search code examples
c++language-lawyerunionsmemmove

Is it legal to std::memmove() from one union member to another?


This code is an attempt to reinterpret memory contents as a different type without violating strict aliasing rules. It was suggested as an answer to "Using std::memmove to work around strict aliasing?", and it may be worth reading the comments there before answering.

#include <cstring>
#include <cstdint>
#include <iomanip>
#include <iostream>

struct Parts {
    std::uint16_t v[2u];
};

static_assert(sizeof (Parts) == sizeof (std::uint32_t));
static_assert(alignof(Parts) <= alignof(std::uint32_t));

int main()
{
    union {
        std::uint32_t u;
        Parts p;
    };
    u = 0xdeadbeef;
    std::clog << std::hex << u << " ~> ";

    std::memmove(&p, &u, sizeof u);
    std::clog << p.v[0] << ", " << p.v[1] << '\n';
}

I don't believe that the std::memmove() call is legal because class.union.general.2 says that at most one of the non-static data members of an object of union type can be active at any time. But my belief doesn't seem to be shared.

Is this legal? Which parts of the standard permit/prohibit this?


Solution

  • std::memmove is defined to implicitly create and start the lifetime of implicit-lifetime objects (and before the bytes of the original object are copied into this new object). See [cstring.syn]/3.

    Starting the lifetime of a union member subobject is equivalent with it becoming the active member of the union. See [class.union.general]/2

    So, as long as p (and all of its subobjects) are implicit-lifetime types, as they are here, there is no problem. The behavior is well-defined. p will be the active member after memmove.

    However, a better way of achieving this is simply to use std::start_lifetime_as which is guaranteed to not actually require any access to the memory location.

    Also note that you don't need the union. Under the exact same conditions as above, you can simply memmove from a uint32_t to itself and then reinterpret_cast + std::launder the original pointer to a Parts*. Of course, just as with the union case, you can't use the original uint32_t object anymore afterwards.

    std::memmove and std::memcpy are magic functions that effectively enforce a strict aliasing barrier across their calls for the affected memory. You can only access the memory as one fixed type before the memmove call and one fixed type after the call, but these two types need not be the same (as long as the new type is implicit-lifetime and trivially-copyable). This is also similar in C, where these functions can "erase" the effective type of memory. A user-written function that copies bytes from one object to the other can not have this same effect.

    All of the above assumes that the size and alignment requirements you state in the static_asserts are satisfied, that the involved types are also implicitly-copyable (otherwise std::memmove doesn't have defined behavior in the first place) and that the object representation stored in the original object is valid and produces a value for the target type (which in case of your specific types follows from the previous requirements together with specific requirements placed on the uintXX_t aliased types).