I had heard that std::bit_cast
will be in C++20, and I am slightly puzzled about the conclusion that implementing it necessarily requires special compiler support.
To be fair, the argument I have heard is that the implementation performs a memcpy
operation, and memcpy
is not typically constexpr
, while std::bit_cast
is supposed to be, so making std::bit_cast
constexpr
supposedly requires compiler support for a constexpr
-compliant memcpy
operation.
I was wondering, however, if it was possible to implement a compliant bit_cast
(ie, defined behavior, to same extent that using memcpy
would have had defined behavior) without actually invoking memcpy
at all.
Consider the following code:
template<typename T, typename U>
inline constexpr T bit_cast(const U & x) noexcept {
static_assert(std::is_trivial<T>::value && std::is_trivial<U>::value, "Cannot use bit_cast with non-trivial data" );
static_assert(sizeof(T) == sizeof(U), "bit_cast must be used on identically sized types");
union in_out {
volatile U in;
volatile T out;
inline constexpr explicit in_out(const U &x) noexcept : in(x)
{
}
};
return in_out(in_out(x)).out;
}
Volatile members are used here to force the compiler to emit the necessary code that would write or read from the members, disabling optimizations, and while I know ordinarily assigning to one member of a union and reading from another in the same union is undefined behavior, the C++ standard does appear to allow reading from any member of a union IF it has been bytewise copied from another instance of the exact same union. In the above code, this is effectively accomplished by explicitly calling the default copy constructor on the newly constructed instance that happens to intialize the in
data member. Since the above union contains all trivial types, calling the default copy constructor on it amounts to a bytewise copy, so reading from the out
member of the newly constructed instance should not still be undefined behavior, should it?
Of course, it's entirely possible that I'm missing something extremely obvious here... I certainly can't claim to be any smarter than the people who develop these standards, but if someone can tell me exactly which undefined behavior I am invoking here, I'd really like to know about it.
One of the things you're not allowed to do during constant evaluation is, from [expr.const]/4.9:
an lvalue-to-rvalue conversion that is applied to a glvalue that refers to a non-active member of a union or a subobject thereof;
Which is what your implementation does, so it's not a viable implementation strategy.
Volatile members are used here to force the compiler to emit the necessary code
If you need to write volatile
to make sure the compiler doesn't optimize out your code then your code is not ok. A compiler cannot modify the observable behavior of a valid code. If what you wanted to do (sans volatile
) would have been defined behavior, the compiler would not have been allowed to optimize out the writes and reads you want to force with volatile.
The UB comes from the fact that you are only allowed to read the active member of an union (in
in your example) but you read the inactive one (out
in your example).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With