This question is about the definedness or otherwise of assigning an uninitalised automatic variable to another one of the same type.
Consider
typedef struct
{
int s1;
int s2;
} Foo;
typedef union
{
int u1;
Foo u2;
} Bar;
int main()
{
{
int a;
int b = a; // (1)
}
{
Foo a;
Foo b = a; // (2)
}
{
Bar a;
a.u1 = 0;
Bar b = a; // (3)
}
}
Referring to the comments in main:
(1) is undefined since a is uninitialised. That much I know.
But what about (2)? The struct members s1 and s2 are uninitialised.
Furthermore, what about (3)? The memory u2.s2 is uninitialised, so reading it is undefined behaviour no?
The behavior is undefined in (1) and (2).
Per the C standard, the value of an object with automatic storage duration that is not initialized is indeterminate (C 2011 [N1570] 6.7.9 10). Nominally, this means it has some value, but we do not know what it is while writing the program.
However, the standard also says “If the lvalue designates an object of automatic storage duration that could have been declared with the register storage class (never had its address taken), and that object is uninitialized (not declared with an initializer and no assignment to it has been performed prior to use), the behavior is undefined” (6.3.2.1 2). In your sample code, the address of a is never taken, and it is not initialized, and using it in an expression is an lvalue. Therefore, the behavior is undefined.
(This passage, 6.3.2.1 2, was designed to accommodate processors that can detect use of an uninitialized register. Nonetheless, the rule in the C standard applies to all implementations.)
(3) is not clearly addressed by the C standard. Although a member of the union has been assigned a value, and hence is not uninitialized for purposes of 6.3.2.1 2, the object being used in b = a is the union, not its member. Obviously, our intuitive notion is that, if a member of a union is assigned a value, the union has a value. However, I do not see this specified in the C standard.
We can infer 6.3.2.1 2 is not intended to consider a union or structure to be uninitialized, at least if part of it has been assigned a value, because:
Structures can have unnamed members, such as unnamed bit fields.
Per C 6.7.9 9, unnamed members of structures have indeterminate value, even after initialization (of the structures).
If 6.3.2.1 2 applied to structures in which not every member had been assigned a value, then b = a would always be undefined if a were a structure with an unnamed member and had automatic storage duration.
That seems unreasonable and not what the standard intended.
However, there is some wiggle room here. The standard could have specified that a structure is not uninitialized only if it were initialized or all of its named members have been assigned values. In that case (3) would be undefined if a were a structure in which only one member had been assigned a value. I do not think this wiggle room exists with a union; if a member of the union has been assigned a value, it is only reasonable to consider the union not to be uninitialized.
In general, assigning from an uninitialized object isn't undefined behavior, it only makes the result unspecified.
But the code you show indeed has undefined behavior -- for a different reason than you assume. Citing N1570 (latest C11 draft), §6.3.2.1 p2 here:
[...] If
the lvalue designates an object of automatic storage duration that could have been
declared with the register storage class (never had its address taken), and that object
is uninitialized (not declared with an initializer and no assignment to it has been
performed prior to use), the behavior is undefined.
Explaining this a bit: The C standard is prepared to handle values that aren't stored in an addressable location. This is typically the case when they are held in one of the CPU's registers. Explicitly giving an object the register storage class is only a hint to the compiler that it should, if sensible, hold that object in a register. The other way around, a compiler is free to hold any object with automatic storage duration in a register as long as the code doesn't need to address it (by taking a pointer).
In your code, you have uninitialized objects with automatic storage duration that never have their address taken, so the compiler would be free to place them in registers. This means there is no value for the object (not even an unspecified one) before it is initialized. Therefore, using this potentially non-existent value to initialize another object (or, for other purposes) is undefined behavior.
If your code would take a pointer to the respective a in all these examples, the result of the assignment would be unspecified (of course), but the behavior would be defined.
It's worth to add that structs and unions have nothing to do with the answer to your question. The rules are the same for all kind of objects with automatic storage duration. That said, in your third example, a isn't uninitialized any more, after you assign one member of the union. So for your third example, the behavior is well-defined. It doesn't matter what's in the other member of the union, a union can only hold a value for one of its members at a time.
Related
An obvious example of undefined behavior (UB), when reading a value, is:
int a;
printf("%d\n", a);
What about the following examples?
int i = i; // `i` is not initialized when we are reading it by assigning it to itself.
int x; x = x; // Is this the same as above?
int y; int z = y;
Are all three examples above also UB, or are there exceptions to it?
Each of the three lines triggers undefined behavior. The key part of the C standard, that explains this, is section 6.3.2.1p2 regarding Conversions:
Except when it is the operand of the sizeof operator, the
_Alignof operator, the unary & operator, the ++ operator, the
-- operator, or the left operand of the . operator or an
assignment operator, an lvalue that does not have array type
is converted to the value stored in the designated object
(and is no longer an lvalue); this is called lvalue
conversion. If the lvalue has qualified type, the value has
the unqualified version of the type of the lvalue; additionally,
if the lvalue has atomic type, the value has the non-atomic version
of the type of the lvalue; otherwise, the value has the
type of the lvalue. If the lvalue has an incomplete type and does
not have array type, the behavior is undefined. If the lvalue
designates an object of automatic storage duration that could
have been declared with the register storage class (never had its
address taken), and that object is uninitialized (not declared
with an initializer and no assignment to it has been
performed prior to use), the behavior is undefined.
In each of the three cases, an uninitialized variable is used as the right-hand side of an assignment or initialization (which for this purpose is equivalent to an assignment) and undergoes lvalue to rvalue conversion. The part in bold applies here as the objects in question have not been initialized.
This also applies to the int i = i; case as the lvalue on the right side has not (yet) been initialized.
There was debate in a related question that the right side of int i = i; is UB because the lifetime of i has not yet begun. However, that is not the case. From section 6.2.4 p5 and p6:
5 An object whose identifier is declared with no linkage and without the storage-class specifier static has automatic
storage duration, as do some compound literals. The result of
attempting to indirectly access an object with automatic storage
duration from a thread other than the one with which the object is
associated is implementation-defined.
6 For such an object that does not have a variable length array type, its lifetime extends from entry into the block
with which it is associated until execution of that block ends in any
way. (Entering an enclosed block or calling a function
suspends, but does not end,execution of the current block.) If
the block is entered recursively, a new instance of the object is
created each time. The initial value of the object is
indeterminate. If an initialization is specified for the
object, it is performed each time the declaration or compound
literal is reached in the execution of the block; otherwise,
the value becomes indeterminate each time the declaration is reached
So in this case the lifetime of i begins before the declaration in encountered. So int i = i; is still undefined behavior, but not for this reason.
The bolded part of 6.3.2.1p2 does however open the door for use of an uninitialized variable not being undefined behavior, and that is if the variable in question had it's address taken. For example:
int a;
printf("%p\n", (void *)&a);
printf("%d\n", a);
In this case it is not undefined behavior if:
The implementation does not have trap representations for the given type, OR
The value chosen for a happens to not be a trap representation.
In which case the value of a is unspecified. In particular, this will be the case with GCC and Microsoft Visual C++ (MSVC) in this example as these implementations do not have trap representations for integer types.
Use of the not initialized automatic storage duration objects invokes UB.
Use of the not initialized static storage duration objects is defined as they are initialized to 0s
int a;
int foo(void)
{
static int b;
int c;
int d = d; //UB
static int e = e; //OK
printf("%d\n", a); //OK
printf("%d\n", b); //OK
printf("%d\n", c); //UB
}
In cases where an action on an object of some type might have unpredictable consequences on platforms where the type has trap representations, but have at-least-somewhat predictable behavior for types that don't, the Standard will seek to avoid distinguishing platforms that do or don't define the behavior by throwing everything into the catch-all category of "Undefined Behavior".
With regard to the behavior of uninitialized or partially-initialized objects, I don't think there's ever been a consensus over exactly which corner cases must be treated as though objects were initialized with Unspecified bit patterns, and which cases need not be treated in such fashion.
For example, given something like:
struct ztstr15 { char dat[16]; } x,y;
void test(void)
{
struct zstr15 hey;
strcpy(hey.dat, "Hey");
x=hey;
y=hey;
}
Depending upon how x and y will be used, there are at least four ways it might be useful to have an implementation process the above code:
Squawk if an attempt is made to copy any automatic-duration object that isn't fully initialized. This could be very useful in cases where one must avoid leakage of confidential information.
Zero-fill all unused portions of hey. This would prevent leakage of confidential information on the stack, but wouldn't flag code that might cause such leakage if the data weren't zero-filled.
Ensure that all parts of x and y are identical, without regard for whether the corresponding members of hey were written.
Write the first four bytes of x and y to match those of hey, but leave some or all of the remaining portions holding whatever they held before test() was called.
I don't think the Standard was intended to pass judgment as to whether some of those approaches would be better or worse than others, but it would have been awkward to write the Standard in a manner that would define behavior of test() while allowing for option #3. The optimizations facilitated by #3 would only be useful if programmers could safely write code like the above in cases where client code wouldn't care about the contents of x.dat[4..15] and y.dat[4..15]. If the only way to guarantee anything about the behavior of that function would be to write all portions of hey were written, including those whose values would be irrelevant to program behavior, that would nullify any optimization advantage approach #3 could have offered.
Is this undefined behaveiour because in the For loop the variable i has no initial value?
#include <stdio.h>
static int i;
int foo(int i)
{
int ret = i;
for(int i; i<4;i++){
ret+=i;
}
return ret;
}
int main() {
printf("%d", i+foo(4));
return 0;
}
Main Answer
Is this undefined behaveiour because in the For loop the variable i has no initial value?
Yes. C 2018 6.3.2.1 says that using the value of i in this situation has undefined behavior.
In more detail, static int i; defines an object i with static storage duration. Objects with static storage duration are “created” (in the C model of computing) when your program starts and are initialized with zero if no explicit initializer is given for them.
Then int foo(int i) defines a parameter i that is initialized when the function is called.
Then for(int i; i<4;i++) defines an object i that is not initialized. This i has automatic storage duration (which is the default for objects defined inside functions without a storage-class keyword like static). Due to a rule in the C standard, it is also relevant that your program never takes the address of this i, as with using the & operator on it. That rule is in C 2018 6.3.2.1 2, and it talks about the process of getting the value of an object:
… If the lvalue designates an object of automatic storage duration that could have been declared with the register storage class (never had its address taken), and that object is uninitialized (not declared with an initializer and no assignment to it has been performed prior to use), the behavior is undefined.
In i<4, i is the lvalue that sentence speaks of, and the program evaluates it when starting the for loop. Because of the rule above, this evaluation has undefined behavior.
Supplement
Suppose inside the for loop you had the simple statement &i;, which merely evaluates the address of i and does nothing with it. Then the rule cited above would not apply, because taking the address of an object prevents declaring it with register. In this case, another rule applies. 6.2.4 6 says uninitialized objects of automatic storage duration have indeterminate values:
… The initial value of the object is indeterminate…
Indeterminate means an object may behave as though it has a different value each time it is used, or it can be a trap representation. Using a trap representation is another way to have undefined behavior, but many C implementations do not have trap representations for int objects these days. In this case, each time your program evaluates the i<4 test in the for or the i++ update or the ret+=i in the body, the C standard allows the program to behave as if i has a new value.
In this case, a variety of outcomes are allowed by the C standard. Some of them lead to undefined behavior in your program due to integer overflow. Others could lead to the function looping indefinitely or executing the loop as if i had been initialized to zero. Although the behavior is not formally undefined, it is not defined well enough to predict what the program will do.
In the for loop scope i is a not initialized automatic storage variable which initilial value is indeterminate. There result of operation using this variable is undefined
— The value of an object with automatic storage
duration is used while it is indeterminate (6.2.4, 6.7.9,
6.8)
6.7.9 p10 If an object that has automatic storage duration is not initialized explicitly, its value is indeterminate.
The "struct inheritance technique" in C (as described in this question) is made possible by the fact that the C standard guarantees that the first member of a struct will never have any padding before it (?), and that the address of the first member will always be equal to the address of the struct itself.
This allows usage such as the following:
typedef struct {
// some fields
} A;
typedef struct {
A base;
// more fields
} B;
typedef struct {
B base;
// yet more fields
} C;
C* c = malloc(sizeof(C));
// ... init c or whatever ...
A* a = (A*) c;
// ... access stuff on a etc.
B* b = (B*) c;
// ... access stuff on b etc.
This question has two parts:
A. It seems to me this technique breaks the strict aliasing rule. Am I wrong, and if so, why?
B. Suppose that this technique is indeed legal. In that case, does it make a difference if A: we first store the object in an lvalue of its specific type, before down or up casting it to a different type, or B: if we cast it directly to the particular type desired at the moment, without first storing it in the lvalue of the specific type?
For example, are these three options all equally legal?
Option 1:
C* make_c(void) {
return malloc(sizeof(C));
}
int main(void) {
C* c = make_c(); // First store in a lvalue of the specific type
A* a = (A*) c;
// ... do stuff with a
C* c2 = (C*) a; // Cast back to C
// ... do stuff with c2
return 0;
}
Option 2:
C* make_c(void) {
return malloc(sizeof(C));
}
int main(void) {
A* a = (A*) make_c(); // Don't store in an lvalue of the specific type, cast right away
// ... do stuff with a
C* c2 = (C*) a; // Cast back to C
// ... do stuff with c2
return 0;
}
Option 3:
int main(void) {
A* a = (A*) malloc(sizeof(C)); // Don't store in an lvalue of the specific type, cast right away
// ... do stuff with a
C* c2 = (C*) a; // Cast to C - even though the object was never actually stored in a C* lvalue
// ... do stuff with c2
return 0;
}
A. It seems to me this technique breaks the strict aliasing rule. Am I wrong, and if so, why?
Yes, you are wrong. I'll consider two cases:
Case 1: The C is fully initialized
That would be this, for example:
C *c = malloc(sizeof(*c));
*c = (C){0}; // or equivalently, "*c = (C){{{0}}}" to satisfy overzealous compilers
In that case, all the bytes of the representation of a C are set, and the effective type of the object comprising those bytes is C. This comes from paragraph 6.5/6 of the standard:
If a value is stored into an object having no declared type through an
lvalue having a type that is not a character type, then the type of
the lvalue becomes the effective type of the object for that access
and for subsequent accesses that do not modify the stored value.
But structure and array types are aggregate types, which means that objects of such types contain other objects within them. In particular, each C contains a B identified as its member base. Because the allocated object is, at this point, effectively a C, it contains a sub-object that is effectively a B. One syntax for an lvalue referring to that B is c->base. The type of that expression is B, so it is consistent with the strict-aliasing rule to use it to access the B to which it refers. That has to be ok, else structures (and arrays) would not work at all, whether dynamically allocated or not.*
But, as discussed in my answer to your previous question, (B *)c is guaranteed to be equal (in value and type) to &c->base. Thus *(B *)c is another lvalue referring to the B that is the first member of *c. That the syntax of that expression is different from that of the previous lvalue we considered is of no account. It is an lvalue of type B, associated with an object of type B, so using it to access the object to which it refers is one of the cases allowed by the SAR.
None of this is any different from the statically and automatically allocated cases.
Case 2: The C is not fully initialized
That could be something like this:
C *c = malloc(sizeof(*c));
*(B *)c = (B){0};
We have thereby assigned to the initial B-sized portion of the allocated object via an lvalue of type B, so the effective type of that initial portion is B. The allocated space does not at this point contain an object of (effective) type C. We can access the B and its members, read or write, via any acceptably-typed lvalues referring to them, as discussed above. But we have a strict aliasing violation if we
attempt to read *c as a whole (e.g. C c2 = *c;);
attempt to read C members other than base (e.g. X x = c->another;); or
attempt to read the allocated object via an lvalue of most unrelated types (e.g. Unrelated_but_not_char u = *(Unrelated_but_not_char *) c;
The first two of those cases are of interest here, and they make sense in terms of the dynamically allocated object, when interpreted as a C, not being fully initialized. Similar incomplete-initialization cases can arise with automatically allocated objects, too; they also produce undefined behavior, but by different rules.
Note well, however, that there is no strict aliasing violation for any write to the allocated space, because any such write will (re)assign the effective type of (at least) the region that is written to.
And that brings us to the main tricksome bit. What if we do this:
C *c = malloc(sizeof(*c));
c->base = (B){0};
? Or this:
C *c = malloc(sizeof(*c));
c->another = 0;
The allocated object does not have any effective type before the first write to it (and in particular, it does not have effective type C), so do write-to-member expressions via *c even make sense? Are they well-defined? The letter of the standard might support an argument that they do not, but no implementation adopts such interpretation, and there is no reason to think that any ever would.
The interpretation most consistent with both the letter of the standard and universal practice is that writing through a member-access lvalue constitutes simultaneously writing to the member and to its host aggregate, thus setting the effective type of the whole region, even though only one member's value is written. Of course, that still does not make it ok to read members whose values have not been written -- because their values are indeterminate, not because of the SAR.
That leaves this case:
C *c = malloc(sizeof(*c));
*(B *)c = (B){0};
B b2 = c->base; // What about this?
That is, if the effective type of an initial region of the allocated space is B, can we use a member-access lvalue based on type C to read the stored value of that B region? Again, one might argue not, on the basis that there is no actual C, but in practice, no implementation makes that interpretation. The effective type of the object being read -- the initial region of the allocated space -- is the same as the type of the lvalue used for access, so in that sense there is no SAR violation. That the host C is wholly hypothetical is a question primarily of syntax, not semantics, because the same region can definitely be read as an object of the same type via an alternative expression.
* But the SAR nevertheless forestalls any debate on this point by providing that "an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union)" is among the types that may be accessed. This clears any ambiguity surrounding the position that accessing a member also constitutes accessing any objects containing it.
I believe this quote from C11 (ISO/IEC 9899:2011 §6.5 7) should answer some of your questions (my emphasis added):
An object shall have its stored value accessed only by an lvalue expression that has one of the following types:
— a type compatible with the effective type of the object,
— a qualified version of a type compatible with the effective type of the object,
— a type that is the signed or unsigned type corresponding to the effective type of the object,
— a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
— an aggregate or union type that includes one of the aforementioned types among its
members (including, recursively, a member of a subaggregate or contained union), or
— a character type.
Then more can be answered by this (ISO/IEC 9899:2011 §6.7.2.1 15):
A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a structure object, but not at its beginning.
The rest can be answered by this snippet (ISO/IEC 9899:2011 §7.22.3 1):
The order and contiguity of storage allocated by successive calls to the
aligned_alloc, calloc, malloc, and realloc functions is unspecified. The pointer returned if the allocation succeeds is suitably aligned so that it may be assigned to a pointer to any type of object with a fundamental alignment requirement and then used to access such an object or an array of such objects in the space allocated (until the space is explicitly deallocated).
In conclusion:
A. You're wrong. See the first and second quotes for the reasoning.
B. No, it doesn't make a difference. See the third quote (and maybe the first) for the reasoning.
Yes, the first element of the structure doesn't have any padding before it.
Second, when the type of an anonymous field is a typedef for a struct or union, code may refer to the field using the name of the typedef.
This is a good practice taken from the GCC manuals:
typedef struct {
// some fields
} A;
typedef struct {
A;
// more fields
} B;
typedef struct {
B;
// yet more fields
} C;
B get_B (struct C *c) { return c->B; } /* access B */
Please check Unnamed Structure and Union Fields
AFAIK, there are three situations where aliasing is ok
Types that only differ by qualifier or sign can alias each other.
struct or union types can alias types contained inside them.
casting T* to char* is ok. (the opposite is not allowed)
These makes sense when reading simple examples from John Regehrs blog posts but I'm not sure how to reason about aliasing-correctness for larger examples, such as malloc-like memory arrangements.
I'm reading Per Vognsens re-implementation of Sean Barrets stretchy buffers. It uses a malloc-like schema where a buffer has associated metadata just before it.
typedef struct BufHdr {
size_t len;
size_t cap;
char buf[];
} BufHdr;
The metadata is accessed by subtracting an offset from a pointer b:
#define buf__hdr(b) ((BufHdr *)((char *)(b) - offsetof(BufHdr, buf)))
Here's a somewhat simplified version of the original buf__grow function that extends the buffer and returns the buf as a void*.
void *buf__grow(const void *buf, size_t new_size) {
// ...
BufHdr *new_hdr; // (1)
if (buf) {
new_hdr = xrealloc(buf__hdr(buf), new_size);
} else {
new_hdr = xmalloc(new_size);
new_hdr->len = 0;
}
new_hdr->cap = new_cap;
return new_hdr->buf;
}
Usage example (the buf__grow is hidden behind macros but here it's in the open for clarity):
int *ip = NULL;
ip = buf__grow(ip, 16);
ip = buf__grow(ip, 32);
After these calls, we have 32 + sizeof(BufHdr) bytes large memory area on the heap. We have ip pointing into that area and we have new_hdr and buf__hdr pointing into it at various points in the execution.
Questions
Is there a strict-aliasing violation here? AFAICT, ip and some variable of type BufHdr shouldn't be allowed to point to the same memory.
Or is it so that the fact that buf__hdr not creating an lvalue means it's not aliasing the same memory as ip? And the fact that new_hdr is contained within buf__grow where ip isn't "live" means that those aren't aliasing either?
If new_hdr were in global scope, would that change things?
Do the C compiler track the type of storage or only the types of variables? If there is storage, such as the memory area allocated in buf__grow that doesn't have any variable pointing to it, then what is the type of that storage? Are we free to reinterpret that storage as long as there is no variable associated with that memory?
Is there a strict-aliasing violation here? AFAICT, ip and some variable of type BufHdr shouldn't be allowed to point to the same memory.
What's important to remember is that a strict aliasing violation only occurs when you do a value access of a memory location, and the compiler believes that what's stored at that memory location is of a different type. So it is not so important to speak of the types of the pointers, as to speak of the effective type of whatever they point at.
An allocated chunk of memory has no declared type. What applies is C11 6.5/6:
The effective type of an object for an access to its stored value is the declared type of the
object, if any. 87)
Where note 87 clarifies that allocated objects have no declared type. That is the case here, so we continue to read the definition of effective type:
If a value is stored into an object having no declared type through an
lvalue having a type that is not a character type, then the type of the lvalue becomes the
effective type of the object for that access and for subsequent accesses that do not modify
the stored value.
This means that as soon as we do an access to the chunk of allocated memory, the effective type of whatever is stored there, becomes the type of whatever we stored there.
The first time access happens in your case, is the lines new_hdr->len = 0; and new_hdr->cap = new_cap;, making the effective type of the data at those addresses size_t.
buf remains inaccessed, so that part of the memory does not yet have an effective type. You return new_hdr->buf and set an int* to point there.
The next thing that will happen, I assume is buf__hdr(ip). In that macro, the pointer is cast to (char *), then some pointer subtraction occurs:
(b) - offsetof(BufHdr, buf) // undefined behavior
Here we formally get undefined behavior, but for entirely different reasons than strict aliasing. b is not a pointer pointing to the same array as whatever is stored before b. The relevant part is the specification of the additive operators 6.5.6:
For subtraction, one of the following shall hold:
— both operands have arithmetic type;
— both operands are pointers to qualified or unqualified versions of compatible complete object types; or
— the left operand is a pointer to a complete object type and the right operand has integer type.
The first two clearly don't apply. In the third case, we don't point to a complete object type, as buf has not yet gotten an effective type. As I understand it, this means we have a constraint violation, I'm not entirely sure here. I am however very sure that the following is violated, 6.5.6/9:
When two pointers are subtracted, both shall point to elements of the same array object, or one past the last element of the array object; the result is the difference of the subscripts of the two array elements. The size of the result is implementation-defined,
and its type (a signed integer type) is ptrdiff_t defined in the <stddef.h> header.
If the result is not representable in an object of that type, the behavior is undefined
So that's definitely a bug.
If we ignore that part, the actual access (BufHdr *) is fine, since BufHdr is a struct ("aggregate") containing the effective type of the object accessed (2x size_t). And here the memory of buf is accessed for the first time, getting the effective type char[] (flexible array member).
There is no strict aliasing violation unless you would after invoking the above macro go and access ip as an int.
If new_hdr were in global scope, would that change things?
No, the pointer type does not matter, only the effective type of the pointed-at object.
Do the C compiler track the type of storage or only the types of variables?
It needs to track the effective type of the object if it wishes to do optimizations like gcc, assuming strict aliasing violations never occur.
Are we free to reinterpret that storage as long as there is no variable associated with that memory?
Yes you can point at it with any kind of pointer - since it is allocated memory, it doesn't get an effective type until you do a value access.
The Standard does not define any means by which an lvalue of one type can be used to derive an lvalue of a second type that can be used to access the storage, unless the latter has a character type. Even something as basic as:
union foo { int x; float y;} u = {0};
u.x = 1;
invokes UB because it uses an lvalue of type int to access the storage associated with an object of type union foo and float. On the other hand, the authors of the Standard probably figured that since no compiler writer would be so obtuse as to use the lvalue-type rules as justification for not processing the above in useful fashion, there was no need to try to craft explicit rules mandating that they do so.
If a compiler guarantees not to "enforce" the rule except in cases where:
an object is modified during a particular execution of a function or loop;
lvalues of two or more different types are used to access storage during such execution; and
neither lvalue has been visibly and actively derived from the other within such execution
such a guarantee would be sufficient to allow a malloc() implementation that would be free of "aliasing"-related problems. While I suspect the authors of the Standard probably expected compiler writers to naturally uphold such a guarantee whether or not it was mandated, neither gcc nor clang will do so unless the -fno-strict-aliasing flag is used.
Unfortunately, when asked in Defect Report #028 to clarify what the C89 rules meant, the Committee responded by suggesting that an lvalue formed by dereferencing a pointer to a unions member will mostly behave like an lvalue formed directly with the member-access operator, except that actions which would invoke Implementation-Defined Behavior if done directly on a union member should invoke UB if done on a pointer. When writing C99, the Committee decided to "clarify" things by codifying that principle into C99's "Effective Type" rules, rather than recognizing any cases where an lvalue of a derived type may be used to access the parent object [an omission which the Effective Type rules do nothing to correct!].
I am trying to decipher a note that led to a change between C99 and C11.
The change proposed in that note ended up in C11's 6.2.4:8, namely:
A non-lvalue expression with structure or union type, where the
structure or union contains a member with array type (including,
recursively, members of all contained structures and unions) refers
to an object with automatic storage duration and temporary lifetime.
Its lifetime begins when the expression is evaluated and its initial
value is the value of the expression. Its lifetime ends when the
evaluation of the containing full expression or full declarator ends.
Any attempt to modify an object with temporary lifetime results in
undefined behavior.
I understand why the change was needed (some discussion can be found here. Note that the discussion goes back to before C11). However, what I don't understand is a side remark that Clark Nelson made in writing his note:
Please note that this approach additionally declares an example like
this, which was conforming under C99, to be non-conforming:
struct X { int a[5]; } f();
int *p = f().a;
printf("%p\n", p);
I understand why this example is non-conforming under C11. What I specifically fail to understand is how it is conforming under C99. And, if it is defined under C99, what is it supposed to do then, definedly print the value of a dangling pointer?
My understanding is that in C99, the finest grain of lifetime for an object is the block. Thus, while 6.5.2.2 (and some other § mentioned in the note you refer to) specifically says that you can't access the returned value after the next sequence point, technically its address is not indeterminate until after you have left the enclosing block (the reason why you should have some storage reserved for an inaccessible object is left as an exercise for the reader, though). Thus, something like
struct X { int a[5]; } f();
int *p;
{ p = f().a; }
printf("%p\n", p);
is undefined in C99 as well as in C11. In C11, the notion of "temporary lifetime", that does not exist in C99, allows to consider that the pointer becomes indeterminate as soon as the full expression ends.