4

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.

2 Answers 2

7

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.

Sign up to request clarification or add additional context in comments.

2 Comments

But I'm not actually referring to the "inactive" member. That would only be applicable to the original constructed instance where I assign the in field. I then invoke a copy constructor, which does a bytewise copy of the entire union, and all fields are apparently permitted afterwards [basic.types]/3. The standard specifies calling memcpy, but calling the copy constructor should do the same thing, shouldn't it?
@markt1964 Yes you are. The section you're citing has to do with memcpying a T, it has nothing to do with unions or suddenly treating a T as a U. The copy constructor doesn't magically change anything (and your code isn't invoking a copy constructor anyway - in_out(in_out(x)) means the same thing as in_out(x) since C++17)
3

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

5 Comments

Actually, I read from the out member from an instance that was bitwise copied from another instance of the same union, so the code doesn't read from an "inactive" member. I seem to remember reading somewhere that the standard explicitly permits reading from any field of a union that has been bytewise copied from another instance of the same type, which is what I effectively do by explicitly invoking the copy constructor.
@markt1964 I don't know of such a rule. I know a union can have only one active member. C indeed lets you use unions for type punning and most compilers let you use unions for type punning in C++ with their extension, but in standard C++ you cannot.
@markt1964 this has nothing to do with your case. It's about memcopy-ing from and to the same type.
As an addendum, I discovered that if I did not make the members volatile, if there were alignment restrictions on one of the types, with optimizations enabled, it would generate a fault at runtime. The compiler basically tried to optimize away the assignment and just tried to use the original data that was not yet aligned properly by passing it through the union. For everything else, it worked just fine. Putting volatile in forced the compiler to use the union and the data was aligned correctly, even for types that had tight restrictions.
@markt1964: “It works for me” is not relevant here.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.