c++lifetimestdlaunder

C++ Undefined behaviour when reinterpret cast is used


The following piece of code is undefined behavior since the lifetime of Data is not started

#include <iostream>
#include <cstring>
struct Data{

};
void process(char * buff){
    decltype(auto) d = std::launder(reinterpret_cast<Data *>(buff));
}
int main() {
     char buf[sizeof(Data)];
     process(buf);
}

Reading the cppreference of memmove says the following:

std::memmove may be used to implicitly create objects in the destination buffer.

My question is if the piece of code above is changed to

#include <iostream>
#include <cstring>
struct Data{

};
void process(char * const buff){
    decltype(auto) d = std::launder(reinterpret_cast<Data *>(std::memmove(buff,buff,sizeof(Data))));
}
int main() {
   char buf[sizeof(Data)];
   process(buf);
}

Is the lifetime of Data implicitly started during memmove and no Undefined Behaviour ? If yes when the lifetime is ended at this example?


Solution

  • TL;DR
    In general, it's still UB because you don't move an object representation from a proper object to the destination buffer.


    A possible exception is if Data is of implicit-lifetime (NB it's the case for an empty struct such as in the question example).
    You'll have to change your buffer declaration to unsigned char buf[sizeof(Data)] or std::byte buf[sizeof(Data)], and change process signature accordingly.
    In this case, memmove is not necessary either as the bytes array creation is enough to start lifetime of such objects: see here.
    Notice that, in this case, Data is still uninitialized.

    Otherwise, what you can do is a placement new on buf:

    int main() {
       unsigned char buf[sizeof(Data)];
       new(buf) Data; // creates a Data object at buf, be careful of alignment
       // here I'm using default constructor, other constructors can be used.
       process(buf); // don't need memmove there
    }
    

    Notice that the object representation of the new Data object is unrelated to any value previously stored in the buffer. It will depend on the constructor used.

    Beside, as mentioned in the snippet, be careful of alignment. If Data is over-aligned you are still UB or you will have to use a larger buffer and std::align (or a simpler solution, see below).

    Anyway this design is dangerous as, from inside process, you have no mean to be sure that a Data object actually lives at buf.


    Thanks to @user17732522 comment, a way to have better guarantees on alignment would be:

    alignas(Data) unsigned char buf[sizeof(Data)];
    

    Thanks to @user17732522 comment, another option, could be std::start_lifetime_as from C++23 but you should have a valid object representation inside the buffer (getting such a representation is a different story) otherwise the object value will be unspecified.

    The current wording of the question is not precise enough regarding the actual use-case to tell what would be the best solution.