Let me start by a different point of failure in the OP:
void *p = malloc(sizeof f);
Since it's being compilded as C++ code, this snnipet produces a very common type of UB. Standard C functions may or may not be present in global namespace when standard C++ headers are included. So, the program may or may not compile. Even if it compiles, there's no guarantee that the behavior matches that of either of C or C++ specifications. Preprocessor may act in unpredicted ways. Standard identifiers shared between C and C++, must always be qualified as std::, even if a using directive or declaration has already been introduced:
auto p = std::malloc(sizeof f);
assert(p);
Having solved this not so-obvious problem, let's break the focal point of the question down:
int *q = std::memcpy(p, &f, sizeof f);
AFAIK, the above line always fails at compile-time - due to the simple fact that void* is not implicitly convertible to any other type than its own cv-qualified forms. So, an explicit casting syntax is required. And the simplest working one is static_cast:
auto q = static_cast<int*>
( std::memcpy(p, &f, sizeof f) );
My preference is to automatically deduce the type of result after an explicit cast to avoid repetion and accidental invocation of an extra implicit cast. But that's not a strict rule because of other possibilities for error.
Now I further replace the single-line code above with a roughly equivalent snippet:
std::memcpy(p, &f, sizeof f);
auto q = static_cast<int*>(p);
Because of previously:
float f = 1.5f;
We know that p points to a valid instance of float which is not a CV-qualified form of int and being a fundamental type, has no inheritance/refinement relationship with int. That means the cast violates strict alaising rule, hence invokes UB.
std::memcpy can only legally circumvent strict alaising for its parameters, but it has a restriction of std::is_trivially_copyable_v<S> on the source, and a std::is_trivially_copyable_v<D> and std::is_trivially_default_constructible_v<D> on its destination; for fundamental types, the above constraints are satisfied by default. The return value is only a copy of the destination pointer.
In order to use the return type as a different implicit life-time type, you need a lifetime cast:
auto q = std::start_lifetime_as<int>
( std::memcpy(p, &f, sizeof f) );
Now q points to in int object, but as long as q is active, p should not be used on the same object.
A simpler approach would be to use a static_cast on std::malloc, before std::memcpy:
auto p = static_cast<int*>
( std::malloc(sizeof f) );
assert(p);
std::memcpy(p, &f, sizeof f);
auto value = *p;
Now it's possible to apply the static_cast to the output of std::memcpy. But what's the point when p had the correct type? The above snippet is correct, because std::malloc is assumed(by the standard) to start the lifetime of implicit life-time types - which covers are fundamental types.
But if all you want is type punning, it's much easier in C++20:
#include <bit>
auto constexpr value = std::bit_cast<int>(1.5f);
The requirements are similar to what was mentioned for source and destination of std::memcpy: source and destination must be trivial types with same size in memory.
As you can see, std::bit_cast can be evaluated at compile-time too. So you don't need an external code generator to compute magic numbers as the bit representation of instances of source type. It's simple, easy to use, and readable. Explicit use of std::mem* functions is not encouraged anymore. This low-level utilities are meant to be buried behind well-tested expertly-written libraries. All their use cases are already covered by other C++ features.
std::memcpyreturns avoid*and assigning that toint*is a compiler error. GCC says, "error: invalid conversion from 'void*' to 'int*' [-fpermissive]"float, as that is how the program is preparing the allocated memory. The program is not allocating or copying anint, it is allocating and copying afloat. So the implicit object is afloat, not anint. Simply pointing anint*pointer at the allocated memory does not magically turn the implicitfloatobject into anintobject.language-lawyer. I am curious which of the 2 answers is actually correct. I don't like the last part of "that operation implicitly creates and starts the lifetime of zero or more objects of implicit-lifetime types in its specified region of storage if doing so would result in the program having defined behavior". Canmemcpyactually considerstatic_cast<int*>to decide which object type to create? Cast of returned value is already out of scope ofmemcpyfunction.static_cast<int*>" .... usually a cast doesn't itself generate UB, it's the dereference. So it's the*qthat must be considered. And yea, the point of implicit lifetime types was to give something within C++'s object model that looks a bit like C's weaker model: if it looks like an int, there must have been an int there. See the original paper: open-std.org/jtc1/sc22/wg21/docs/papers/2020/p0593r6.html