c++type-punningmemmove

memmove in-place change of effective type (type-punning)


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?


Solution

  • 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.