c++language-lawyerc++20undefined-behaviorlifetime

Is it technically UB to static_cast<A*>(memmove(dst, (void*)src, sizeof(src))) since C++20?


Consider the following excerpt from wg21.link/p0593:

A call to memmove behaves as if it

  • copies the source storage to a temporary area

  • implicitly creates objects in the destination storage, and then

  • copies the temporary storage to the destination storage.

This permits memmove to preserve the types of trivially-copyable objects, or to be used to reinterpret a byte representation of one object as that of another object.

struct A { int n; };
auto a = A{1};
void* p1 = &a;
alignas(A) char buf[8];

// p1 is of void*, so it shouldn't be assumed pointing to an object of A.
auto p2 = static_cast<A*>(memmove(buf, p1, sizeof(A))); 
p2->n = 2; // Is this technically UB since C++20?

auto fn = [&] { return memmove(buf, p1, sizeof(A)); };
// Must it be `auto p3 = std::start_lifetime_as<A>(fn());` here?
auto p3 = static_cast<A*>(fn()); // fn is not blessed by the standard as memmove.
p3->n = 3; // Is this technically UB since C++20?

Solution

  • Object existence doesn't depend on the compiler knowing the type. In the magical abstract machine, objects can exist without any code having the type.

    // p1 is of void*, so it shouldn't be assumed pointing to an object of A.
    auto p2 = static_cast<A*>(memmove(buf, p1, sizeof(A))); 
    p2->n = 2; // Is this technically UB since C++20?
    

    the wg link provided means that the target of memmove (buf) gets unspecified objects created. It does not depend on the arguments of memmove or the type of p1; in fact, the type of the object created is any type and number of objects that could make the program have defined behavior.

    So for p2->n to be valid, it must have created at least one object of type A in the storage. There is only room for one, so it must have created one. The memory of the object was overwritten with the memory pointed by p1 - as it happens, there is an A object there (doesn't matter what the type of p1 is, it matters if there was actually an object there), and you can move A object state by copying bytes.

    So the only solution is that memmove created an object of type A within buf, which in turn makes p2->n defined behavior.

    Is this magical? Yes it is.

    The goal is to permit object creation based type analysis by the compiler. The "special" functions mean that certain assumptions about object creation have to be made by the compiler on that memory - that objects (of some uncertain type and number) have been created there. The types end up having to be certain restricted types (basically, objects that can come into existence and be destroyed without any machine code running) in both theory and practice.

    This is designed to retroactively justify a bunch of C-style code in C++ programs, while still permitting some type-based object lifetime analysis compilers want to be able to do.