https://gcc.gnu.org/bugzilla/show_bug.cgi?id=95349
--- Comment #5 from Andrew Downing <andrew2085 at gmail dot com> --- (In reply to Richard Biener from comment #1) > I think std::launder merely acts as optimization barrier here and without we > manage to propagate the constant. We still "miscompile" things dependent on > what exactly the C++ standard says. When seeing > > std::uint64_t* s3(double* p) { > unsigned char storage[sizeof(std::uint64_t)]; > std::memcpy(storage, p, sizeof(storage)); > auto t = new(p) std::uint64_t; > std::memcpy(t, storage, sizeof(storage)); > return t; > } > > the placement new has no effect (it's just passing through a pointer) and > memcpy has C semantics, transfering the active dynamic type of 'p' > through 'storage' back to 'p'. > > std::uint64_t f3() { > double d = 3.14159; > return *s3(&d); > } > > ... which is still 'double'. Which you then access via an lvalue of type > uint64_t which invokes undefined behavior. So in GCCs implementation > reading of relevant standards you need -fno-strict-aliasing and your program > is not conforming. > > So what goes on is that GCC optimizes s3 to just { return (uint64t *)p; } > which makes f3 effectively do > > double d = 3.14159; > return *(uint64_t *)&d; > > which arguably is bogus. Without the std::launder we are nice to the > user and "optimize" the above to return the correct value. With > std::launder we cannot do this since it breaks the pointer flow and > we'll DSE the initialization of 'd' because it is not used (due to the > undefinedness in case the load would alias it). The placement new (in each of s1/s2/s3) shouldn't do nothing though. That line should create a std::uint64_t object in the storage of the double in each of f1/f2/f3, and end the lifetime of the double. Physically it does nothing, but in the C++ object model, it ends the lifetime of the double, and begins the lifetime of a std::uint64_t. Since the default trivial constructor is used no initialization is done, and the std::uint64_t has indeterminate value (according to the standard). The next line copies the object representation of the double, which was saved to separate storage, to the std::uint64_t. That is allowed with std::memcpy because they are both trivially copyable types. Operations that implicitly create objects only do so if it would give the program defined behavior. If the first std::memcpy implicitly creates a double object in storage, no harm done, but if the second std::memcpy implicitly ends the lifetime of the std::uint64_t pointed to by 't' and begins the lifetime of a double in it's place that wouldn't give the program defined behavior because 't' is being returned as a std::uint64_t* and is later dereferenced. Also, I'm not sure if operations that implicitly create objects in storage are allowed to do so if an object has already explicitly created in that storage (from new).