Reputation: 27028
In the following question:
What's a proper way of type-punning a float to an int and vice-versa?, the conclusion is that the way to construct doubles from integer bits and vise versa is via memcpy
.
That's fine, and the pseudo_cast
conversion method found there is:
template <typename T, typename U>
inline T pseudo_cast(const U &x)
{
static_assert(sizeof(T) == sizeof(U));
T to;
std::memcpy(&to, &x, sizeof(T));
return to;
}
and I would use it like this:
int main(){
static_assert(std::numeric_limits<double>::is_iec559);
static_assert(sizeof(double)==sizeof(std::uint64_t));
std::uint64_t someMem = 4614253070214989087ULL;
std::cout << pseudo_cast<double>(someMem) << std::endl; // 3.14
}
My interpretation from just reading the standard and cppreference is/was that is should also be possible to use memmove
to change the effective type in-place, like this:
template <typename T, typename U>
inline T& pseudo_cast_inplace(U& x)
{
static_assert(sizeof(T) == sizeof(U));
T* toP = reinterpret_cast<T*>(&x);
std::memmove(toP, &x, sizeof(T));
return *toP;
}
template <typename T, typename U>
inline T pseudo_cast2(U& x)
{
return pseudo_cast_inplace<T>(x); // return by value
}
The reinterpret cast in itself is legal for any pointer (as long as cv is not violated, item 5 at cppreference/reinterpret_cast). Dereferencing however requires memcpy
or memmove
(§6.9.2), and T and U must be trivially copyable.
Is this legal? It compiles and does the right thing with gcc and clang.
memmove
source and destinations are explicitly allowed to overlap, according
to cppreference std::memmove and memmove,
The objects may overlap: copying takes place as if the characters were copied to a temporary character array and then the characters were copied from the array to dest.
Edit: originally the question had a trivial error (causing segfault) spotted by @hvd. Thank you! The question remains the same, is this legal?
Upvotes: 7
Views: 747
Reputation: 11
This should be legal in C++20. Example in godbolt.
template <typename T, typename U>
requires (
sizeof(U) >= sizeof(T) and
std::alignment_of_v<T> <= std::alignment_of_v<U> and
std::is_trivially_copyable_v<T> and
std::is_trivially_destructible_v<U>
)
[[nodiscard]] T& reinterpret_object(U& obj)
{
// Get access to object representation
std::byte* bytes = reinterpret_cast<std::byte*>(&obj);
// Copy object representation to temporary buffer.
// Implicitly create a T object in the destination storage. The lifetime of U object ends.
// Copy temporary buffer back.
void* storage = std::memmove(bytes, bytes, sizeof(T));
// Storage pointer value is 'pointer to T object', so we are allowed to cast it to the proper pointer type.
return *static_cast<T*>(storage);
}
reinterpret_cast
to a different pointer type is allowed (7.6.1.10)
An object pointer can be explicitly converted to an object pointer of a different type.
Accessing the object representation through an std::byte*
pointer is allowed (7.2.1)
If a program attempts to access the stored value of an object through a glvalue whose type is not similar to one of the following types the behavior is undefined
- a char, unsigned char, or std::byte type.
std::memmove
behaves as-if copying to a temporary buffer and can implicitly create objects (21.5.3)
The functions memcpy and memmove are signal-safe. Both functions implicitly create objects ([intro.object]) in the destination region of storage immediately prior to copying the sequence of characters to the destination.
Implicit object creation is described in (6.7.2)
Some operations are described as implicitly creating objects within a specified region of storage. For each operation that is specified as implicitly creating objects, that operation implicitly creates and starts the lifetime of zero or more objects of implicit-lifetime types ([basic.types]) in its specified region of storage if doing so would result in the program having defined behavior. If no such set of objects would give the program defined behavior, the behavior of the program is undefined. If multiple such sets of objects would give the program defined behavior, it is unspecified which such set of objects is created. [Note 4: Such operations do not start the lifetimes of subobjects of such objects that are not themselves of implicit-lifetime types. — end note]
Further, after implicitly creating objects within a specified region of storage, some operations are described as producing a pointer to a suitable created object. These operations select one of the implicitly-created objects whose address is the address of the start of the region of storage, and produce a pointer value that points to that object, if that value would result in the program having defined behavior. If no such pointer value would give the program defined behavior, the behavior of the program is undefined. If multiple such pointer values would give the program defined behavior, it is unspecified which such pointer value is produced.
It is not specified that std::memmove
is such a function and its returned pointer value would be a pointer to the implicitly created object.
But it makes sense that is is so.
Returning a pointer to the new object is allowed by (7.6.1.9)
A prvalue of type “pointer to cv1 void” can be converted to a prvalue of type “pointer to cv2 T”, where T is an object type and cv2 is the same cv-qualification as, or greater cv-qualification than, cv1. If the original pointer value represents the address A of a byte in memory and A does not satisfy the alignment requirement of T, then the resulting pointer value is unspecified. Otherwise, if the original pointer value points to an object a, and there is an object b of type T (ignoring cv-qualification) that is pointer-interconvertible with a, the result is a pointer to b. Otherwise, the pointer value is unchanged by the conversion.
If std::memmove
does not return a usable pointer value, std::launder<T>(reinterpret_cast<T*>(bytes))
(17.6.5) should be able to produce such a pointer value.
Additional notes:
I'm not 100% sure if all the requires
are correct or some condition is missing.
To get zero overhead, the compiler must to optimize the std::memmove
away (gcc and clang seem to do it).
The lifetime of the original object ends (6.7.3)
A program may end the lifetime of any object by reusing the storage which the object occupies or by explicitly calling a destructor or pseudo-destructor ([expr.prim.id.dtor]) for the object.
This means that using the original name or pointers or references to it will result in undefined behaviour.
The object can be "revived" by reinterpreting it back reinterpret_object<U>(reinterpret_object<T>(obj))
and that should allow using the old references (6.7.3)
If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if the original object is transparently replaceable (see below) by the new object. An object o1 is transparently replaceable by an object o2 if:
- the storage that o2 occupies exactly overlays the storage that o1 occupied, and
- o1 and o2 are of the same type (ignoring the top-level cv-qualifiers), and
- o1 is not a complete const object, and
- neither o1 nor o2 is a potentially-overlapping subobject ([intro.object]), and
- either o1 and o2 are both complete objects, or o1 and o2 are direct subobjects of objects p1 and p2, respectively, and p1 is transparently replaceable by p2.
The object representations should be "compatible", interpreting the bytes of the original object as bytes of the new one can produce "garbage" or even trap representations.
Upvotes: 1
Reputation: 69882
My reading of the standard suggests that both these functions will result in UB.
consider:
int main()
{
long x = 10;
something_with_x(x*10);
double& y = pseudo_cast_inplace<double>(x);
y = 20;
something_with_y(y*10);
}
Because of the strict alias rule, it seems to me that there's nothing to stop the compiler from reordering instructions to produce code as-if:
int main()
{
long x = 10;
double& y = pseudo_cast_inplace<double>(x);
y = 20;
something_with_x(x*10); // uh-oh!
something_with_y(y*10);
}
I think the only legal way to write this is:
template <typename T, typename U>
inline T pseudo_cast(U&& x)
{
static_assert(sizeof(T) == sizeof(U));
T result;
std::memcpy(std::addressof(result), std::addressof(x), sizeof(T));
return result;
}
Which in reality results in the exact same assembler output (i.e. none whatsoever - the entire function is elided, as are the variables themselves) - at least on gcc with -O2
Upvotes: 3
Reputation: 18051
Accessing a double
while the actual type is uint64_t
is undefined behavior because compiler will never consider that an object of type double
can share the address of an object of type uint64_t
intro.object:
Unless an object is a bit-field or a base class subobject of zero size, the address of that object is the address of the first byte it occupies. Two objects a and b with overlapping lifetimes that are not bit-fields may have the same address if one is nested within the other, or if at least one is a base class subobject of zero size and they are of different types; otherwise, they have distinct addresses.
Upvotes: 0
Reputation:
C++ does not allow a double
to be constructed merely by copying the bytes. An object first needs to be constructed (which may leave its value uninitialised), and only after that can you fill in its bytes to produce a value. This was underspecified up to C++14, but the current draft of C++17 includes in [intro.object]:
An object is created by a definition (6.1), by a new-expression (8.3.4), when implicitly changing the active member of a union (12.3), or when a temporary object is created (7.4, 15.2).
Although constructing a double
with default initialision does not perform any initialisation, the construction does still need to happen. Your first version includes this construction by declaring the local variable T to;
. Your second version does not.
You could modify your second version to use placement new
to construct a T
in the same location that previously held an U
object, but in that case, when you pass &x
to memmove
, it is no longer required to read the bytes that had made up x
's value, because the object x
has already been destroyed by the earlier placement new
.
Upvotes: 5