There are (or at least were on the C90) two modifications for making this behavior undefined. The first was that the compiler would be allowed to generate additional code that kept track of what was in the union and generated a signal when you accessed the wrong member. In practice, I don’t think anyone ever (maybe CenterLine?). Others were optimization opportunities that open up and they are being used. I used compilers that will delay the record until the last possible moment, which may not be necessary (since the variable goes out of scope, or there is a subsequent record of a different value). Logically, one would expect that this optimization would be turned off when the union was visible, but it was not the earliest version of Microsoft C.
Problems like punning are complex. Committee C (back in the late 1980s) more or less took the position that you should use casts (in C ++, reinterpret_cast) for this, and not even though both methods were widespread at that time. Since then, some compilers (g ++, for example) have taken the opposite view, supporting the use of unions, but not the use of castings. And in practice, it does not work if it is not immediately obvious that there is type-punning. This may be the motivation of the g ++ point of view. If you are a union member, it immediately becomes apparent that there may be tip-puns. But of course, something like:
int f(const int* pi, double* pd) { int results = *pi; *pd = 3.14159; return results; }
called with:
union U { int i; double d; }; U u; ui = 1; std::cout << f( &u.i, &u.d );
is completely legal in accordance with strict rules, standard, but with a g ++ error (and, possibly, many other Compilers); when compiling f compiler assumes that pi and pd cannot have an alias and reorders the entry in *pd , but read with *pi . (I believe that this has never been guaranteed. But the current wording of the standard guarantees this.)
EDIT:
As other answers claim the behavior is actually (largely based on quoting a non-normative note taken out of context):
The correct answer is here: pablo1977: the standard makes no attempt to determine the behavior when using the punning function. The probable reason for this is that there is not one that he could identify. This does not prevent the implementation from defining it; although I don’t remember any specific discussions on this subject, I’m sure the Goal was that implementations define something (and most, if not all, do).
Regarding the use of concatenation for type-punning: when C was developing the C90 (in the late 1980s), it was a clear intention to allow a debugging implementation that has additional validation (for example, using fat pointers for border validation). From the discussions at the time, it was clear that the debugging implementation could cache information about the last value initialized in the union, and a trap if you try to access anything else. This is clearly stated in §6.7.2.1 / 16: "The value of not more than one member can be stored in the combined object at any time." Access to the value that there is no undefined behavior; it can assimilate access to an uninitialized variable. (There was some discussion at the time as to whether a member of the same type was legal or not. I do not know that however a final decision was made; after 1990 I switched to C ++.)
Regarding the quotation from C89 saying that behavior is a definition: its definition in section 3 (terms, definitions and symbols) seems very strange. I will have to watch this in my copy of the C90 at home; the fact that it was filmed in later versions of the standards, the presence of the committee was considered a mistake.
The use of unions supported by the standard is a means of imitating derivation. You can define:
struct NodeBase { enum NodeType type; }; struct InnerNode { enum NodeType type; NodeBase* left; NodeBase* right; }; struct ConstantNode { enum NodeType type; double value; };
and legally access to base.type, although Node was initialized via inner . (The fact that §6.5.2.3 / 6 begins with “One special guarantee is made ...” and further explicitly authorize this very strong indication that all other cases must be undefined. And, of course, there is a statement about that Undefined behavior is indicated differently in this International Standard by the words “undefined behavior or by omitting any explicit definition of behavior” in § 4/2, to state that the behavior is not undefined, you must show where it is defined in the standard.)
Finally, in relation to the type-penalty: all (or at least all of this I used) implementations somehow support it. my impression at that time was that the goal was that pointer casting is a way to support implementation; in C ++ standard, there is even (non-normative) text to suggest that the results of a reinterpret_cast will be "unsurprising" for someone familiar with the underlying architecture. In practice, however, most implementations support the use of a union for type-punning, provided that access is through a union member. Most implementations (but not g ++) also support pointer casting, provided that casting a pointer is clearly visible to the compiler (for some undefined pointer definition). And the “standardization” of basic equipment means things like:
int getExponent( double d ) { return ((*(uint64_t*)(&d) >> 52) & 0x7FF) + 1023; }
actually quite portable. (It will not work on mainframes, of course.) What doesn't work, things like my first example, where aliases are invisible to the compiler. (I am pretty sure that this is a defect in the standard. It seems that I remember even seeing DR regarding this.)