Is reinterpret_cast type punning actually undefined behavior?

I believe your misunderstanding lies here:

This reads to me as an explicit statement that the result of reinterpret_cast<T&>(v) is an lvalue reference to an object of type T, to which access is clearly “through a glvalue of” “the dynamic type of the object”.

[basic.lval]/8 is a bit misleading because it talks about the dynamic type “of the object” when the dynamic type is actually a property of the glvalue [defns.dynamic.type] used to access the object rather than the object itself. Essentially, the dynamic type of the glvalue is the type of the object that is currently living in the place that the glvalue refers to (effectively, the type of the object that was constructed/initialized in that piece of memory) [intro.object]/6. For example:

float my_float = 42.0f;
std::uint32_t& ui = reinterpret_cast<std::uint32_t&>(my_float);

here, ui is a reference that refers to the object created by the definition of my_float. Accessing this object through the glvalue ui would invoke undefined behavior (per [basic.lval]/8.1), however, because the dynamic type of the glvalue is float while the type of the glvalue is std::uint32_t.

There are few valid uses of a reinterpret_cast like that, but use cases other than just casting to void* and back exist (for the latter, static_cast would be sufficient, as you noted yourself). [basic.lval]/8 effectively gives you a complete list of what they are. For example, it would be valid to examine (and even copy if the dynamic type of the object is trivially-copyable [basic.types]/9) the value of an object by casting the address of the object to char*, unsigned char*, or std::byte* (not signed char*, however). It would be valid to reinterpret_cast an object of signed type to access it as its corresponding unsigned type and vice versa. It would also be valid to cast a pointer/reference to a union to a pointer/reference to a member of that union and access that member through the resulting lvalue if that member is the active member of the union…

The main reason why type punning through casts like this is undefined in general is that making it defined behavior would prohibit some extremely vital compiler optimizations. If you’d allow any object of any type to simply be accessed through an lvalue of any other type, then the compiler would have to assume that any modification of an object through some lvalue can potentially affect the value of any object in the program unless it can prove otherwise. As a result, it would basically be impossible, for example, to keep stuff around in registers for any useful period of time because any modification of anything would immediately invalidate whatever you may have in registers at the moment. Yes, any good optimizer will perform aliasing analysis. But, while such methods certainly work and are powerful, they can, out of principle, only cover a subset of cases. Disproving or proving aliasing in general is basically impossible (equivalent to solving the halting problem I would think)…

Leave a Comment