Is the strict aliasing rule incorrectly specified?
Asked Answered
H

9

30

As previously established, a union of the form

union some_union {
    type_a member_a;
    type_b member_b;
    ...
};

with n members comprises n + 1 objects in overlapping storage: One object for the union itself and one object for each union member. It is clear, that you may freely read and write to any union member in any order, even if reading a union member that was not the last one written to. The strict aliasing rule is never violated, as the lvalue through which you access the storage has the correct effective type.

This is further supported by footnote 95, which explains how type punning is an intended use of unions.

A typical example of the optimizations enabled by the strict aliasing rule is this function:

int strict_aliasing_example(int *i, float *f)
{
    *i = 1;
    *f = 1.0;
    return (*i);
}

which the compiler may optimize to something like

int strict_aliasing_example(int *i, float *f)
{
    *i = 1;
    *f = 1.0;
    return (1);
}

because it can safely assume that the write to *f does not affect the value of *i.

However, what happens when we pass two pointers to members of the same union? Consider this example, assuming a typical platform where float is an IEEE 754 single precision floating point number and int is a 32 bit two's complement integer:

int breaking_example(void)
{
    union {
        int i;
        float f;
    } fi;

    return (strict_aliasing_example(&fi.i, &fi.f));
}

As previously established, fi.i and fi.f refer to an overlapping memory region. Reading and writing them is unconditionally legal (writing is only legal once the union has been initialized) in any order. In my opinion, the previously discussed optimization performed by all major compilers yields incorrect code as the two pointers of different type legally point to the same location.

I somehow can't believe that my interpretation of the strict aliasing rule is correct. It doesn't seem plausible that the very optimization the strict aliasing was designed for is not possible due to the aforementioned corner case.

Please tell me why I'm wrong.

A related question turned up during research.

Please read all existing answers and their comments before adding your own to make sure that your answer adds a new argument.

Hunt answered 5/8, 2016 at 21:42 Comment(19)
@user3386109 I have verified that the optimization I mentioned is actually being performed by modern compilers (I tested: gcc 5, clang 3.8 and an unknown version of SUN Studio). Yes, if restrict was also present the optimization could be no doubt performed. I am however concerned about the case where no restrict is present (as is usually the case).Hunt
What becomes more intriguing is the question of "How does the compiler handle storage of fi in memory?" What is the format of the overlapping object? Dumping to binary only raises more ("You will have to look at the source.") questions. (e.g. both fi.i and fi.f reside at the same location as 00000000010000000000011000010000 (little endian), which is neither an int or a float with a value of 1)Hexagon
Since you aren't actually accessing the union members, this code is no different from the same code just using malloc and type casting. Isn't it? So all the rules for unions don't apply.Qoph
@DavidSchwartz I am accessing union members by taking pointers to them and dereferencing these pointers. I haven't found language that indicates that dereferencing pointers to union members is anyhow different from accessing union members directly.Hunt
@FUZxxl That language is this: "If the member used to read the contents of a union object is not the same as the member last used to store a value in the object ..."Qoph
@DavidSchwartz I don't see any language that says “this only applies if a member designator is used to refer to the union member.” As far as I'm concerned, the usual semantics for pointers apply. That is especially, that accessing an object through a (legally obtained) pointer is identical to accessing that object directly.Hunt
It is hard to see how the house of cards cannot be a violation of some aspect of the standard as it relies on both values being compatible (e.g. *i = 1; and *f = 1.0 in strict_aliasing_example). What if instead you had *f = *i + 1.0;? I can't point to a part of the rule to say "this is what it breaks.", but it seems like it throws the behavior, or result, to the whim of what parameter is sequenced when. Good brain teaser for Friday.Hexagon
@DavidC.Rankin I have specifically chosen an example with clear sequence points as to not make this question about multiple modifications between two sequence points which is well established to be undefined behaviour.Hunt
This is basically the example in DR 236.Erastatus
The function doesn't care, and doesn't know, where the two pointers come from, so it is not reasonable to think that just because those two pointers belong to a union, the function will avoid strict alising. There is no free lunch, if you do want to avoid strict aliasing, you will need to pass the union to the function.Amanda
@T.C., you should make that an answer. It pulls the rug from under every other answer. (...how did you find that??)Atoll
@Erastatus Neato! Please make this an answer. You might want to include the discussions in N973 and N987.Hunt
There are numerous problems with the specification of the strict aliasing rule, particularly in regard to its application to union members. I have a relatively complete run-down in a blog post. It is difficult to properly summarise the various issues in a shorter text.Sisera
@Sisera It would be great if you could write up an answer, preferably with reference to the aforementioned DR 236 and its discussion.Hunt
The example given in the rationale for the C89 aliasing rule involved aliasing between global variable and a pointer to something of a different type; as far as C89 is concerned, I suspect many of the people who approved the rule expected it to only be applicable in such cases (which would be the easiest cases to apply the optimization as well as the ones least likely to break existing code). Trying to apply it more broadly without gutting the language would require rules that are much better written than those of C99 or C11, but C99 tried to pretend it was "clarifying" rules...Padget
...rather than identifying new optimization opportunities that might potentially break some existing code; I don't know think I've seen the C11 rationale, but I've seen no willingness to acknowledge the incompatibility between the semantics some programs require and the semantic limitations that some compilers impose, and the fact that reconciling such incompatibility will require recognizing two or more dialects with different aliasing rules.Padget
@Padget The C11 rationale sadly isn't actually out. You raise very good points though. Care to write up an answer?Hunt
@fuz: Since writing the above, I've come to the conclusion that the real problem is that when C89 leaves behaviors undefined the intention was that implementers would interpret that as an invitation to exercise judgment, rather than as an in indication that that the authors of the Standard had exercised judgment and determined that any code relying upon certain features should be viewed as broken. From what I can tell, an unforgiving reading of C11 would regard an access to any member of a union that contains types that aren't alias-compatible as invoking UB. If implementers use judgment...Padget
...that won't be a problem, of course, since it's obvious that such accesses should be defined. On the other hand, if implementers use judgment based upon an implementations' intended target platform and application field, then implementations for most fields will recognize that certain constructs should be supported regardless of whether the Standard mandates such support.Padget
S
17

Starting with your example:

int strict_aliasing_example(int *i, float *f)
{
    *i = 1;
    *f = 1.0;
    return (*i);
}

Let's first acknowledge that, in the absence of any unions, this would violate the strict aliasing rule if i and f both point to the same object; assuming the object has no declared type, then *i = 1 sets the effective type to int and *f = 1.0 then sets it to float, and the final return (*i) then accesses an object with effective type of float via an lvalue of type int, which is clearly not allowed.

The question is about whether this would still amount to a strict-aliasing violation if both i and f point to members of the same union. For this not to be the case, it would either have to be that there is some special exemption from the strict aliasing rule that applies in this situation, or that accessing the object via *i does not (also) access the same object as *f.

On union member access via the "." member access operator, the standard says (6.5.2.3):

A postfix expression followed by the . operator and an identifier designates a member of a structure or union object. The value is that of the named member (95) and is an lvalue if the first expression is an lvalue.

The footnote 95 referred to in above says:

If the member used to read the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called ‘‘type punning’’). This might be a trap representation.

This is clearly intended to allow type punning via a union, but it should be noted that (1) footnotes are non-normative, that is, they are not supposed to proscribe behaviour, but rather they should clarify the intention of some part of the text in accordance with the rest of the specification, and (2) this allowance for type punning via a union is deemed by compiler vendors as applying only for access via the union member access operator - since otherwise strict aliasing is pretty useless for optimisation, as just about any two pointers potentially refer to different members of the same union (your example is a case in point).

So at this point, we can say that:

  • the code in your example is explicitly allowed by a non-normative footnote
  • the normative text on the other hand seems to disallow your example (due to strict aliasing), assuming that accessing one member of a union also constitutes access to another - but more on this shortly

Does accessing one member of a union actually access the others, though? If not, the strict aliasing rule isn't concerned with the example. (If it does, the strict aliasing rule, problematically, disallows just about any type-punning via a union).

A union is defined as (6.2.5 para 20):

A union type describes an overlapping nonempty set of member objects

And note that (6.7.2.1 para 16):

The value of at most one of the members can be stored in a union object at any time

Since access is (3):

〈execution-time action〉 to read or modify the value of an object

... and, since non-active union members do not have a stored value, then presumably accessing one member does not constitute access to the other members!

However, the definition of member access (6.5.2.3, quoted above) says "The value is that of the named member" (this is the precise statement that footnote 95 is attached to) - if the member has no value, what then? Footnote 95 gives an answer but as I've noted it is not supported by the normative text.

In any case, nothing in the text would seem to imply that reading or modifying a union member "via the member object" (i.e. directly via an expression using the member access operator) should be any different than reading or modifying it via pointer to that same member. The consensus understanding applied by compiler vendors, which allows them to perform optimisations under the assumption that pointers of different types do not alias, and that requires type punning be performed only via expressions involving member access, is not supported by the text of the standard.

If footnote 95 is considered normative, your example is perfectly fine code without undefined behaviour (unless the value of (*i) is a trap representation), according to the rest of the text. However, if footnote 95 is not considered normative, there is an attempted access to an object which has no stored value and the behaviour then is at best unclear (though the strict aliasing rule is arguably not relevant).

In the understanding of compiler vendors currently, your example has undefined behaviour, but since this isn't specified in the standard it's not clear exactly what constraint the code violates.

Personally, I think the "fix" to the standard is to:

  • disallow access to a non-active union member except via lvalue conversion of a member access expression, or via assignment where the left-hand-side is a member access expression (an exception to this could perhaps be made for when the member in question has character type, since that would not have an effect on possible optimisations due to a similar exception in the strict aliasing rule itself)
  • specify in the normative text that the value of a non-active member is as is currently described by footnote 95

That would make your example not a violation of the strict aliasing rule, but rather a violation of the constraint that a non-active union member must be accessed only via an expression containing the member access operator (and appropriate member).

Therefore, to answer your question - Is the strict aliasing rule incorrectly specified? - no, the strict aliasing rule is not relevant to this example because the objects accessed by the two pointer dereferences are separate objects and, even though they overlap in storage, only one of them has a value at a time. However, the union member access rules are incorrectly specified.

A note on Defect Report 236:

Arguments about union semantics invariably refer to DR 236 at some point. Indeed, your example code is superficially very similar to the code in that Defect Report. I would note that:

  1. The example in DR 236 is not about type-punning. It is about whether it is ok to assign to a non-active union member via a pointer to that member. The code in question is subtly different to that in the question here, since it does not attempt to access the "original" union member again after writing to the second member. Thus, despite the structural similarity in the example code, the Defect Report is largely unrelated to your question.
  2. "Committee believes that Example 2 violates the aliasing rules in 6.5 paragraph 7" - this indicates that the committee believes that writing a "non-active" union member, but not via an expression containing a member access of the union object, is a strict-aliasing violation. As I've detailed above, this is not supported by the text of the standard.
  3. "In order to not violate the rules, function f in example should be written as" - i.e. you must use the union object (and the "." operator) to change the active member type; this is in agreement with the "fix" to the standard I proposed above.
  4. The Committee Response in DR 236 claims that "Both programs invoke undefined behavior". It has no explanation for why the first does so, and its explanation for why the 2nd does so seems to be wrong.
Sisera answered 6/8, 2016 at 12:3 Comment(9)
Having the existence of a union serve as a signal that a compiler should allow for common-initial-sequence punning of the indicated types makes perfect sense if one needs to provide a means of telling a compiler that without inventing a new syntax for the purpose, especially since it would be unusual for a program to include two structure types within a complete union type declaration in cases where having a compiler ignore the possibility of CIS punning would be more useful than allowing for that possibility.Padget
Also, it would be helpful if you could cite the parts of the Standard that defines the concept of "active union member" and the circumstances under which it may be changed.Padget
@Padget The C99 standard doesn't use the "active member" language but does say "The value of at most one of the members can be stored in a union object at any time" (6.7.2.1 p14) which amounts to the same thing.Sisera
@Padget "Having the existence of a union serve as a signal that a compiler should allow for common-initial-sequence punning of the indicated types makes perfect sense if one needs to provide a means of telling a compiler that without inventing a new syntax for the purpose" No it doesn't make perfect sense, because it doesn't have the necessary precision; it will apply to the whole compilation unit after the point the union declaration is visible.Sisera
It will apply to the whole compilation unit after the point the union declaration is visible. So? It's not as precise as would be ideal, but much cheaper and better relying upon a "conforming language extension" that disables type-based aliasing entirely to support the kinds of constructs the Common Sequence Rule was meant to facilitate.Padget
@Padget I'm very much against a rule that affects code generation because an unrelated union declaration happens to be visible. I disagree that such a rule makes perfect sense.Sisera
(addendum: I would really prefer that the "member of the same union" requirement was just removed, and allow CIS to type-pun globally, than to have code generation be affected by visibility of a declaration. At least you wouldn't risk strange performance regressions from unrelated changes that way. Admittedly an implementation would be free to implement this way regardless. Anyway, I don't think it's clear what the text in 6.5.2.3 means).Sisera
What is unclear about the definition, beyond the refusal of some compiler writers to accept that concepts like "complete union type declaration" and "visible" have the same meaning as elsewhere in the Standard? There was never a consensus to invite compilers to break the CIS rule in the manner that clang and gcc do, but if the phrases in the rule were interpreted according to their meanings elsewhere it would have been an almost-reasonable compromise between optimization and semantic power.Padget
Even in the absence of aliasing considerations, the CIS rule would be necessary to indicate that the layout of early structure members cannot depend upon later ones. As for why N1570 6.5p7 doesn't talk about common initial sequences, it doesn't make any allowance for accessing a struct or union member with an lvalue of a non-character member type. Under C89, the question of what corner cases to support was regarded as a Quality of Implementation issue, so there was no need to go into detail. C99 pretends to be more formal with additional language like its broken "Effective Type" concept.Padget
U
11

Under the definition of union members in §6.5.2.3:

3 A postfix expression followed by the . operator and an identifier designates a member of a structure or union object. ...

4 A postfix expression followed by the -> operator and an identifier designates a member of a structure or union object. ...

See also §6.2.3 ¶1:

  • the members of structures or unions; each structure or union has a separate name space for its members (disambiguated by the type of the expression used to access the member via the . or -> operator);

It is clear that footnote 95 refers to the access of a union member with the union in scope and using the . or -> operator.

Since assignments and accesses to the bytes comprising the union are not made through union members but through pointers, your program does not invoke the aliasing rules of union members (including those clarified by footnote 95).

Further, normal aliasing rules are violated since the effective type of the object after *f = 1.0 is float, but its stored value is accessed by an lvalue of type int (see §6.5 ¶7).

Note: All references cite this C11 standard draft.

Unclear answered 5/8, 2016 at 22:12 Comment(18)
As already explained by a3f, this fragment concerns §6.5.2.3 ¶6, which is a special rule regarding unions containing structs with common initial sequences. However, my union does not contain and structs, so I don't believe that rule (and the example) is applicable.Hunt
“It is clear that footnote 95 refers to the access of a union member with the union in scope and using the . operator.” —this is not clear to me. Footnote 95 only ever talks about union members. Nothing ever says that a union member can only be referred to by the . operator.Hunt
Furthermore, the part of §J.1 you cite refers to §6.2.6.1 ¶7 which applies when the union members have different lengths and thus there are bytes that are part of some but not all union members. Such bytes indeed have unspecified values. However, I carefully clarified my example (after R Sahu made a remark about §6.2.6.1 ¶7) not to contain such a union as to avoid this unspecified behaviour.Hunt
@FUZxxl Footnote 95 states "If the member used to read the contents of a union object is not the same as the member last used to store a value in the object...." Your function doesn't use a member to store a value in the object, and in turn doesn't use a member to read the contents of the object. You only use objects which correspond to the bytes taken up by the object. Also, you add paragraph numbers to the references in the standard -- this is incorrect; what makes you so sure paragraph 7 was the one of interest? I personally feel that it references paragraph 8 more directly.Unclear
See my first paragraph. Each member is a different object of different effective type, though the member objects share the same storage. Thus the strict aliasing rule does not make the access undefined as the effective type is correct. Strange, but that's what the standard says. The linked question discusses this concept further. §6.5 ¶8 concerns the contraction of floating point expressions, not sure why you think that's relevant.Hunt
@FUZxxl See §6.2.6.1 ¶8. Also, your paragraph is true for accesses to the memory through union members, not via arbitrary pointers.Unclear
Please explain how taking a pointer changes the strict aliasing rule.Hunt
I'm also not sure where you are going with §6.2.6.1 ¶8. In the example I chose, both 1 and 1.0f have exactly one object representation.Hunt
@FUZxxl "Where a value is stored in an object using a type that has more than one object representation for that value, it is unspecified which representation is used, but a trap representation shall not be generated."Unclear
Which of the values involved has more than one object representation? I don't see how that rule applies.Hunt
@FUZxxl I read it as the object using a type that has more than one object representation. In any case, you continue to dismiss every argument posed, and I don't think that has potential for a good discussion. I feel I've made my case.Unclear
This is not correct, the value of a member not written to may have a trap representation(6.2.6.1 5 and 6.5.2.3 3), reading it will cause undefined behavior. This problem is also not related to strict alising, which is the theme of the question..Amanda
You are misreading the point in Annex J. Let me duplicate it with emphasis added: "The following are unspecified: ... The values of bytes that correspond to union members other than the one last stored into". It's saying that the bytes that do not make up the last stored member take unspecified value; the bytes that do make up the last stored member do not have unspecified value (at least, Annex J does not claim that they do). In OP's example the two members could easily be the same size and thus does not then present an issue; the bytes making up the value of both members are the same.Sisera
@Sisera To elaborate, I specifically clarified the example to have two members of equal size with all bits used to make sure that this rule doesn't have an effect on the result.Hunt
I agree with @davmac: the whole "bytes that correspond" thing is something different and not relevant; it's a red herring in the context of this question. The 1st half of this answer is great (albeit entirely self-evident IMO) but is diluted by the 2nd.Sharleensharlene
@Sisera It took a lot of introspection, but I'm inclined to agree with you. Reading that passage now, your interpretation is much more fitting. I will remove that part of the answer and expand upon the first half, but ultimately your answer wins it fair and square here.Unclear
@Unclear thanks. FWIW I have revised my own understanding of the C standard many times over the past few years; it's complex, and it doesn't even always entirely make sense as written.Sisera
@davmac: The C89 Standard will make a lot more sense if one recognizes that C was already in wide use before it was published, and that in cases where quality compilers for some platforms generally defined certain behaviors which were not defined on others, the authors of the Standard did not intend to disturb the status quo. The C89 Standard was never intended to be a complete guide to all the behaviors a quality implementation for any given platform should provide, but was mainly intended to identify a set of behaviors that conforming implementations must provide at any cost.Padget
S
5

The C11 standard (§6.5.2.3.9 EXAMPLE 3) has following example:

The following is not a valid fragment (because the union type is not visible within function f):

 struct t1 { int m; };
 struct t2 { int m; };
 int f(struct t1 *p1, struct t2 *p2)
 {
       if (p1->m < 0)
               p2->m = -p2->m;
       return p1->m;
 }
 int g()
 {
       union {
               struct t1 s1;
               struct t2 s2;
       } u;
       /* ... */
       return f(&u.s1, &u.s2);
 }

But I can't find more clarification on this.

Sanferd answered 5/8, 2016 at 22:10 Comment(5)
@Sanferd The example you cite concerns §6.5.2.3 ¶6. However, that paragraph only concerns unions with structures that have common initial sequences. This is not the case in my example, so I believe the rule does not apply and my code does not exhibit undefined behaviour from that rule.Hunt
@FUZxxl I would say it applies generally. What makes you think it only applies to §6.5.2.3 ¶6 instead of just using structs with initial sequences as an example?Sanferd
@Sanferd Because it specifically explains that the code is invalid because the union type is not visible within f and clearly demonstrates the constraints imposed by §6.5.2.3 ¶6. I still don't see what rule my code violates. As explained before, it can't violate §6.5.2.3 ¶6 as my union does not contain any structures.Hunt
This is the relevant example. Your and @Peter s answers complement each other.Amanda
A relevant example would include a definition of a complete union type before code that accesses the members using freshly-derived pointers to them. Including such an example, however, would require a consensus as to whether its behavior was supposed to be defined, avoided in all correct programs, or supported on a quality-of-implementation basis.Padget
G
4

Essentially the strict aliasing rule describes circumstances in which a compiler is permitted to assume (or, conversely, not permitted to assume) that two pointers of different types do not point to the same location in memory.

On that basis, the optimisation you describe in strict_aliasing_example() is permitted because the compiler is allowed to assume f and i point to different addresses.

The breaking_example() causes the two pointers passed to strict_aliasing_example() to point to the same address. This breaks the assumption that strict_aliasing_example() is permitted to make, therefore results in that function exhibiting undefined behaviour.

So the compiler behaviour you describe is valid. It is the fact that breaking_example() causes the pointers passed to strict_aliasing_example() to point to the same address which causes undefined behaviour - in other words, breaking_example() breaks the assumption that the compiler is allowed to make within strict_aliasing_example().

Grimm answered 5/8, 2016 at 22:23 Comment(6)
The strict aliasing rule as specified in §6.5 ¶7 doesn't mention the word “pointer” anywhere. From which section of the standard do you take that the compiler is allowed to assume that f and i refer to different addresses? I haven't found such a section.Hunt
I'm describing the effect - just as you are. In your example, you're trying to use the strict aliasing rule to justify a claim that the compiler must allow for the possibility that the two pointers of different type point to the same address. I'm describing why that justification doesn't hold.Grimm
@Grimm He understands the effect. He's arguing that his code complies with the standard and thus the effect is a violation of that standard.Qoph
And that's my concern, David. Historically, aliasing rules came to be for various reasons. One reason was allowing compilers to optimise samples like the strict_aliasing_example() as much as possible, so there was effort to define effects or restrictions when aliasing is in play (whether it's via use of unions, typecasts, or whatever). Then there was effort to minimise restrictions. Now we have a language-lawyer discussion interpreting strict aliasing rules as making compiler optimisation of such samples illegal - which completely defeats the original intent.Grimm
@Peter: The original design intention of C was to allow programmers to reinterpret memory in many flexible ways. Since implementations are allowed to use padding bits on all non-char types to trap attempts to write via one type and read via another, the only case compilers are required to allow type punning is with conversions to/from character types (which can't have padding). Because it would be silly for the Standard to mandate that compilers recognize aliasing in cases where they wouldn't be required to allow type punning, the Standard only mandates recognition of aliasing in that case.Padget
@Peter: That should not be taken to imply, however, that the authors of the Standard would not have expected that quality compilers should support other means of type punning in cases where the underlying platform would make them useful. I think the authors of the Standard would have considered absurd the notion that a quality compiler should keep an float cached in a register across a call to something like void bump_exponent(float *f) { *((uint32_t*)f) += 0x01000000; } on platforms where such tricks are useful [note the proximity of the typecast here, versus in the rationale's example].Padget
R
4

The strict aliasing rule forbids access to the same object by two pointers that do not have compatible types, unless one is a pointer to a character type:

7 An object shall have its stored value accessed only by an lvalue expression that has one of the following types:88)

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

In your example, *f = 1.0; is modifying fi.i, but the types are not compatible.

I think the mistake is in thinking that a union contains n objects, where n is the number of members. A union contains only one active object at any point during program execution by §6.7.2.1 ¶16

The value of at most one of the members can be stored in a union object at any time.

Support for this interpretation that a union does not simultaneously contain all of its member objects can be found in §6.5.2.3:

and if the union object currently contains one of these structures

Finally, an almost identical issue was raised in defect report 236 in 2006.

Example 2

// optimization opportunities if "qi" does not alias "qd"
void f(int *qi, double *qd) {
    int i = *qi + 2;
    *qd = 3.1;       // hoist this assignment to top of function???
    *qd *= i;
    return;
}  

main() {
    union tag {
        int mi;
        double md;
    } u;
    u.mi = 7;
    f(&u.mi, &u.md);
}

Committee believes that Example 2 violates the aliasing rules in 6.5 paragraph 7:

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

In order to not violate the rules, function f in example should be written as:

union tag {
    int mi;
    double md;
} u;

void f(int *qi, double *qd) {
    int i = *qi + 2;
    u.md = 3.1;   // union type must be used when changing effective type
    *qd *= i;
    return;
}
Reputation answered 6/8, 2016 at 3:55 Comment(4)
I do not access the same object, I access two different objects that occupy the same storage. See the linked question for why I believe that the union comprises n + 1 objects.Hunt
@FUZxxl I added a link to a defect report that addresses your example. I can see how 6.2.5 leads to your interpretation. The standard could be a little more explicit in this area, but the committee believes the language in the standard is enough to allow the optimization.Reputation
Out of curiosity, is there any reason that any access of the form structValue.member, unionValue.member, structPtr->member, or unionValue->member would not invoke UB if the member is not of a character type? Treating accesses to such lvalues as UB would obviously be dumb, but is there anything in the Standard that would not treat such accesses as violations of 6.5p7? It seems to be that all of the confusion about 6.5p7 stems from the fact that the Standard would be useless if one could never use derived lvalues of non-character types to access values stored in parent objects...Padget
...[which would naturally have different types from the derived lvalues] but the Standard has completely failed to specify any rules that would specify when that is or is not permissible. Passing pointers to two members of the same union and using them to access the same storage in conflicting fashion should be UB not because writing one member and reading another is Implementation-Defined behavior (the rationale given in Defect Report #028), but because the pointers are not associated with a common type in the context where they are used.Padget
S
2

Here is note 95 and its context:

A postfix expression followed by the . operator and an identifier designates a member of a structure or union object. The value is that of the named member, (95) and is an lvalue if the first expression is an lvalue. If the first expression has qualified type, the result has the so-qualified version of the type of the designated member.

(95) If the member used to read the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called “type punning”). This might be a trap representation.

Note 95 clearly applies to an access via a union member. Your code does not do that. Two overlapping objects are accessed via pointers to 2 separate types, none of which is a character type, and none of which is a postfix expression pertinent for type punning.

This is not a definitive answer...

Sabulous answered 5/8, 2016 at 22:9 Comment(13)
The lvalues I use to access the union members refer to object whose effective type is the same type as the declared type of the object they refer to. Therefore, strict aliasing is not violated (cf. ISO 9899:2011 §6.5 ¶7). The important piece is to recognize that the member of the union are distinct objects that merely occupy the same region of storage.Hunt
@FUZxxl But the same principle applies. The compiler would not assume in any other situation that the two pointers might point to the same object, and it will apply strict aliasing optimisations. You do nothing to tell it otherwise, by which I mean you don't access the members via the union, which is what the allowance is for. So, it has no way of knowing said pointers are from a union. Therefore, it can read/write to them as-if they were not at the same address.Sharleensharlene
@Sharleensharlene What language in the standard supports your claim?Hunt
@FUZxxl "union member" is very clearly defined as a usage of the . operator with a union type as the first operand and a member of that type as the second operand.Unclear
@FUZxxl I'm not aware of a precise framing, but I think it flows directly and very specifically from a process of deduction: the normal rules about aliasing, the fact you're passing 2 pointers to the contained types that in any other (read: equivalent) situation are defined as not aliasing, and the fact that the explicitly allowed aliasing must be done via an instance of the union. This seems crystal-clear to me, but if you won't accept deduction because you've already made up your mind what you want the answer to be, there's not a lot I can do.Sharleensharlene
@Sharleensharlene Again: What are the “normal rules of aliasing?” If you mean §6.5 ¶7, that one does not state that an int and a float cannot alias. And yes, I am doing the aliasing through a union. I'm interested in what the standard says, not in what you might think the authors intended when they wrote what it says unless you know that they did.Hunt
@FUZxxl: the example 3 is quite telling of the authors intentions. Yet I agree they should have a simpler example such as your code.Sabulous
@chqrlie: If the authors of the Standard wanted to make their intentions unambiguously clear, they should have included an example in which pointers of different structure types, both of which are members of the same complete visible union type, are used to access shared members. If such access is not permissible, the example in which the union type declaration wasn't visible is pointless. If access is permissible without involving a union-type lvalue at the point of access, that should be illustrated. Personally, I think the authors wanted to avoid being seen as...Padget
...either making it impossible to write practical readable code which can accept pointers to multiple kinds of structures and work with them interchangeably, or as branding existing compiler behaviors as "non-compliant", so they instead left things ambiguous as as to avoid doing either.Padget
@supercat: you are being unnecessarily harsh with the Standard's Committee. I agree they could have made their intentions more clearly. If the code in strict_aliasing_example cannot be optimized, the aliasing rules are useless. They made an exception for unions used explicitly for type-punning. They hint that such use should be explicit in the example. I you feel this is still ambiguous, you should file a defect report.Sabulous
@chqrlie: The way the Standard has been interpreted has shifted over the years, but the language in the Standard has never been adjusted to compensate. There is sufficient variety in hardware platforms and the jobs they are called upon to that any single dialect will either be too anemic to do some of the jobs or require features some hardware platforms cannot practically provide; the solution adopted in 1989 was to recognize a minimal baseline Standard but recognize that quality implementations should go beyond to the extent practical on their individual platforms.Padget
@chqrlie: There should be compilers which--at least when given suitable directives--will use aliasing rules which are stricter than the Standard presently allows, and there should be compilers which--at least when given suitable directives--will use aliasing rules which map operations through memory more precisely than the Standard presently requires. There shouldn't be just one firm aliasing rule that is applicable to all code, so I don't fault the Committee for not wanting to make a single unified hard-line rule. What I do fault is the failure to do anything else...Padget
...to let programmers and compilers coordinate rules that would be most suitable for the task at hand.Padget
T
2

Let's back away from the standard for a second, and think about what's actually possible for a compiler.

Suppose that strict_aliasing_example() is defined in strict_aliasing_example.c, and breaking_example() is defined in breaking_example.c. Assume both of these files are compiled separately and then linked together, like so:

gcc -c -o strict_aliasing_example.o strict_aliasing_example.c
gcc -c -o breaking_example.o breaking_example.c
gcc -o breaking_example strict_aliasing_example.o breaking_example.o

Of course we'll have to add a function prototype to breaking_example.c, which looks like this:

int strict_aliasing_example(int *i, float *f);

Now consider that the first two invocations of gcc are completely independent and cannot share information except for the function prototype. It is impossible for the compiler to know that i and j will point to members of the same union when it generates code for strict_aliasing_example(). There's nothing in the linkage or type system to specify that these pointers are somehow special because they came from a union.

This supports the conclusion that other answers have mentioned: from the standard's point of view, accessing a union via . or -> obeys different aliasing rules compared with dereferencing an arbitrary pointer.

Tusk answered 6/8, 2016 at 3:22 Comment(29)
It's been argued that C's stipulation that members in the common initial sequence can be used interchangeably "anywhere that a definition of the completed type of the union is visible" means compilers must scan all space they can - &, if compatible types X & Y ever coexist in a union, must always assume passed pointers to X and Y can alias, even when there's no evidence. This seems both contrary to previous authors' (before those who added that clause) intentions about accessing via the union, conflated w/ unqualified pointer access - & absurdly difficult/pessimising to implementSharleensharlene
...which seemingly led to most/all C vendors not implementing, & C++ refusing to incorporate, this "declaration is visible" clause - & only implementing 'aliasing' via union member access, not unqualified pointers. See: https://mcmap.net/q/246365/-union-39-punning-39-structs-w-quot-common-initial-sequence-quot-why-does-c-99-but-not-c-stipulate-a-39-visible-declaration-of-the-union-type-39/2757035 But regardless of how poorly this was reasoned & received - it means something bad, & making it good would make it redundant - why would it ever exist if not to allow things that were otherwise forbidden? You can alias union members if, & only if, Z naturally implies as compared to all other situations, where you cannot.Sharleensharlene
More to the point, your last sentence seems meaningless: Accessing union members via . or -> has no relation to aliasing, because you're not accessing them via pointers! It seems to me it's precisely this bizarrely common conflation of two different things - union member access via . or -> vs pointers to things that happen to be union members - that led to all the confusion around these concepts. e.g.: this question and C's 'allowance' (scare quotes as AFAICT this has never really been implemented) for aliasing of members with a common initial sequence. It's made a real mess :|Sharleensharlene
@Sharleensharlene Note that the strict aliasing rule doesn't mention pointers anywhere. I'm not sure why it would hold for access through pointer but not for other accesses to an object.Hunt
@FUZxxl Technically, sure, but there are very few other ways to generate "lvalue expressions" that could ever alias, right? The line about allowing aliasing through a union or aggregate that contains the aliased type either implies (A) accessing via the union, which we know is OK or (B) accessing via a pointer to said union, which would be a type error initially and require casting to work. Even then, by all indications I can see, it would only allow us to alias between int *ia and union u; int *ib = (int *)&u anyway, with no allowance for a union member of an incompatible type.Sharleensharlene
@underscore_d: Much of the usefulness of the common-initial-sequence rule stems the ability to have a routines use pointers to a object of several different types interchangeably, possibly using shared fields to select a type-specific method to use on non-shared fields. Making accesses through pointers of union type would only be practical if the Standard defined the behavior of casting to (union U*) a pointer to an address which was aligned suitably for some but not all members of union U, and then using the resulting pointer to access members for which the pointer was aligned.Padget
@underscore_d: As it is, if function will need to work with pointers to objects that may include 10-byte structures with 2-byte alignment and 4104-byte structures with 8-byte alignment, trying to cast a pointer to the 10-byte structure into a pointer to a union containing both types would likely invoke UB, and thus cannot possibly be a reasonable way of trying to let the compiler know that the pointer might be used to access things of both types.Padget
@Padget Excellent summary of why the common initial sequence was given that allowance - and crucial point that "union member of an incompatible type" is broader than just e.g. int vs float as I oversimplified, as we must also account for alignment... which makes the idea of casting among such pointers a complete non-starter. It seems to have been pretty thoroughly debunked that unions have any privilege in terms of aliasing, and the only unique powers they have are distinct from aliasing but apparently(?) similar-sounding enough that users and even the Committee get awfully confused.Sharleensharlene
@underscore_d: The only interpretation that would give meaning to the requirement that the complete union type be visible is if the existence of a union type containing multiple structure types is regarded as giving a compiler notice that the code is likely to be using pointers of one such structure type to access members of the common initial sequence of another. The authors of that rule may not have considered that many header files include lots of types--including unions--that relatively few consumers need, and the existence of such types might needlessly curtail optimizations.Padget
@underscore_d: The only thing that's "debunked", though, is the notion that the authors of gcc are willing to honor language in the standard which would only allow certain "optimizations" if the compiler is run in a non-compliant mode. Otherwise, I think the real problem is that before restrict became available, some programmers took to defining multiple structures with identical members as a means of letting a compiler know that pointers to things of one type wouldn't be used to access things of another, and a broad interpretation of the CIS would undermine that. The people who approved...Padget
...the Standard have indicated a desire to minimize breakage of existing code, and I doubt many would have voted for the aliasing rules if they knew how gcc would interpret them. If foo->bar is an int, the sub-expression &foo->bar should yield an int* which has no relation whatsoever to the type of foo, but gcc will regard *(int*)(&foo->bar) as being incapable of accessing any int* that isn't the bar field of some structure of foo's type.Padget
@Padget Yup. CIS is massively useful, but the idea of extending it to pointers to such member structs would entail a huge task for vendors &, even if they managed to implement it, huge pessimisation for highly dubious benefit.Sharleensharlene
@Padget But that next bit, I'm finding very hard to believe (which is either your or GCC's fault ;-)... Is it really the case that GCC treats a pointer to struct.Type as a separate 'meta' type from a pointer to Type for purposes of aliasing analyses? That sounds just as ludicrous as the idea of over-interpreting the CIS rule and applying it to pointers... and, while I'm normally all for strict aliasing, that would be taking it so far as to be a caricature. Can you link to any discussions, examples, etc of this?Sharleensharlene
@underscore_d: In most kinds of programs, structures are unlikely to appear as members of a union except when pointers to some union member are going to be used to access others, so outside of cases where union declarations get brought in needlessly via #include files, the rule wouldn't have much adverse effect on performance; if it's necessary to allow compilers to assume union members won't alias, a #pragma STDC xx directive could be added for that purpose.Padget
@underscore_d: As for gcc's behavior, what do you make of godbolt.org/g/hTnDs5 (the test function doesn't reload s1->x after the write to s2->x, despite the explicit address-of and casting operators which should IMHO serve to notify the compiler that some kind of aliasing is likely going on.Padget
@Padget I don't think the program at that link is doing what you think it's doing; did you forget to save? :P But I'm keen to see it. As I read it and the Standard(s) currently, it seems to fly in the face of the allowance for aliasing with lvalues of compatible type, because the type is simply identical, regardless of whether or not one of the pointers incidentally falls within a struct. Assuming no aliasing merely because of the latter seems, frankly, perverse.Sharleensharlene
@underscore_d: The language designed by Dennis Ritchie allowed type punning on platforms where it was meaningful. A language with type punning removed may be useful for many purposes, but is grossly unsuitable for many of the purposes for which C was designed. Many optimizations provided for in C89 can be perfectly usefully applied in systems programming if compilers recognize certain patterns as indications that they need to rein in their optimizers. The proper way to allow more optimizations is not to weaken the language, but add optimization directives like restrict.Padget
@underscore_d: When I posted the previous godbolt example I was focused on the code generated for "test", and forgot to add something to call it. Here's a version that fixes that: godbolt.org/g/AmE4mM (the code for test2() unconditionally returns 5, ignoring the fact that the shared member of the two structures gets written as 6).Padget
@underscore_d: Does the posted example make sense to you? The read_via_s1() function should be reading an lvalue of type int, and the write_via_s2() function should write an lvalue of type int. Unless one interprets the Standard in such a way as to say that an int* can't be used to access a struct member whose type is int (there's no explicit rule allowing such access, but the ability to apply the & operator to non-bitifield non-char struct members would be rather silly without it) I don't see any justification for gcc's behavior within the language of the Standard.Padget
@underscore_d: Otherwise, do you see any practical, readable, and standard-compliant way of adapting code which had been written to take advantage the CIS before compilers gutted it? Under my interpretation, such adaptation would be practical, but I can't see any practical way of adapting code under gcc's interpretation without relying upon gcc-specific directives (which the authors of the Standard can't plausibly have expected programmers to rely upon).Padget
@Padget On my normal browser, all your links to GodBolt take me to a trivial function that just squares the input number. On another, it works. And seems to make me feel a lot better because AFAICT it takes the interpretation of the CIS rule that I want to see, i.e. that it is not eligible for aliasing (unless the accesses are made clearly through the union, which then means we're not really talking about aliasing).Sharleensharlene
@Padget This follows my preferred interpretation, which is how C++ specifies it - & C did till some folk misinterpreted the CIS allowance for reads via union & wrote a DR trying to make it about aliasing - in that as you are passing pointers only to structss, the fact they inhabit some union type somewhere does nothing to change aliasing analyses. Only if accessed via such a union could one safely write one then read the other. Your code is very different from what you originally implied, that an int * to a standalone int could not alias with an int * to a someStruct.int.Sharleensharlene
@underscore_d: Why would the language explicitly specify that the complete declaration must be visible if the member-access operator was required [which would so obviously require full visibility as to make explicit mention absurdly redundant]? What justifies treating the result of the & operator in the example as anything other than an int*? And how is one supposed to adapt code which is supposed to work with the shared parts of a variety of structures that don't all share the same alignment requirements? Making accesses through the union type would be a non-starter if...Padget
...pointers to some of the constituent types may not be aligned suitably for the union type. Given something like struct { int x; short y[5];} foo5 and struct { int x; short y[8]; } foo8, there would be no requirement that foo5.y and foo8.y share the same offset, but on most compilers they would, and compiler writers used to recognize the value of being able to write a function that could accept pointers to all such structures interchangeably.Padget
@Padget You're missing my point: IMO, the "complete declaration" clause is a bad/infeasible idea. That it's unimplemented gives me mixed feelings: on 1 hand, I want compilers to follow the Standard; on the other, I disagree with it! I mainly use C++, so it doesn't affect me much, as it lacks that clause and so aliases how I expect/want. Your claim GCC "treats the result of & in the example as anything other than an int*" is blatantly wrong: aliasing fails between the structs passed to test; saying anything else is diversionary. It fails because GCC share my opinion about said clause.Sharleensharlene
@Padget We can go in circles forever about the utility and context of the clause... but I'm just glad it's definitely not the case that "gcc will regard *(int*)(&foo->bar) as being incapable of accessing any int* that isn't the bar field of some structure of foo's type." Any int * can alias any int *, regardless of struct membership or no. But you're not aliasing between int *s. Before you get to any int *, you've aliased between 2 structs, which - in absence of GCC implementing allowance for aliasing into the CIS - means the separate int * accesses are optimised awaySharleensharlene
@underscore_d: My interpretation of the rule's intent is that if there exists a union declaration which contains both S1 and S2, then an access to a member of S1 which is part of a CIS with S2 should be a potential access to the corresponding member of S2. Unless S1 and S2 are members of the same union type, accesses to S1 and S2 would be presumed independent. In most cases, there wouldn't exist a union type containing arbitrary pairs of structures that weren't going to rely upon the CIS, so I'm not sure why the rule is considered so massively impractical?Padget
@underscore_d: Further, the expression &foo->int is not a read of foo, and it's not a write of foo, and since an access defined as a read or write, the expression isn't an access of foo. The type of the expression is int*, and has nothing to do with the type of foo, so I'm not sure what rule the compiler is using to justify its behavior. Further, you still haven't answered by question about how one should write code that can accept a variety of structure types.Padget
@Padget I think we're talking across each other. I haven't answered the Q as it's not for me to answer, nor do I want to - as I'm not an implementor, don't agree w/ the clause, & haven't found a situation where I feel a need to alias between pointers to different types of struct...just as I haven't felt a (unavoidable) need w/ any other types that are incompatible in C++, C prior to the clause, & C in extant implementations. Having explained where I stand on the rule & reconfirmed the aliasability of int*s, I'm all out. Respectfully, please send further Qs to someone who can answer them!Sharleensharlene
P
2

Prior to the C89 Standard, the vast majority of implementations defined the behavior of write-dereferencing to pointer of a particular type as setting the bits of the underlying storage in the fashion defined for that type, and defined the behavior of read-dereferencing a pointer of a particular type as reading the bits of the underlying storage in the fashion defined for that type. While such abilities would not have been useful on all implementations, there were many implementations where the performance of hot loops could be greatly improved by e.g. using 32-bit loads and stores to operate on groups of four bytes at once. Further, on many such implementations, supporting such behaviors didn't cost anything.

The authors of the C89 Standard state that one of their objectives was to avoid irreparably breaking existing code, and there are two fundamental ways the rules could have been interpreted consistent with that:

  1. The C89 rules could have been intended to be applicable only in the cases similar to the one given in the rationale (accessing an object with declared type both directly via that type and indirectly via pointer), and where compilers would not have reason to expect that lvalues are related. Keeping track for each variable whether it is currently cached in a register is pretty simple, and being able to keep such variables in registers while accessing pointers of other types is a simple and useful optimization and would not preclude support for code which uses the more common type punning patterns (having a compiler interpret a float* to int* cast as necessitating a flush of any register-cached float values is simple and straightforward; such casts are rare enough that such an approach would be unlikely to adversely affect performance).

  2. Given that the Standard is generally agnostic with regard to what makes a good-quality implementation for a given platform, the rules could be interpreted as allowing implementations to break code which uses type punning in ways that would be both useful and obvious, without suggesting that good quality implementations shouldn't try to avoid doing so.

If the Standard defines a practical way of allowing in-place type punning which is not in any way significantly inferior to other approaches, then approaches other than the defined way might reasonably be regarded as deprecated. If no Standard-defined means exists, then quality implementations for platforms where type punning is necessary to achieve good performance should endeavor to efficiently support common patterns on those platforms whether or not the Standard requires them to do so.

Unfortunately, the lack of clarity as to what the Standard requires has resulted in a situation where some people regard as deprecated constructs for which no replacements exist. Having the existence of a complete union type definition involving two primitive types be interpreted as an indication that any access via pointer of one type should be regarded as a likely access to the other would make it possible to adjust programs which rely upon in-place type punning to do so without Undefined Behavior--something which is not achievable any other practical way given the present Standard. Unfortunately, such an interpretation would also limit many optimizations in the 99% of cases where they would be harmless, thus making it impossible for compilers which interpret the Standard that way to run existing code as efficiently as would otherwise be possible.

As to whether the rule is correctly specified, that would depend upon what it is supposed to mean. Multiple reasonable interpretations are possible, but combining them yields some rather unreasonable results.

PS--the only interpretation of the rules regarding pointer-comparisons and memcpy that would make sense without giving the term "object" a meaning different from its meaning in the aliasing rules would suggest that no allocated region can be used to hold more than a single kind of object. While some kinds of code might be able to abide such a restriction, it would make it impossible for programs to use their own memory management logic to recycle storage without excessive numbers of malloc/free calls. The authors of the Standard may have intended to say that implementations are not required to let programmers create a large region and partition it into smaller mixed-type chunks themselves, but that doesn't mean that they intended general-purpose implementations would fail to do so.

Padget answered 10/8, 2016 at 0:44 Comment(0)
P
1

The Standard does not allow the stored value of a struct or union to be accessed using an lvalue of the member type. Since your example accesses the stored value of a union using lvalues whose type is not that of the union, nor any type that contains that union, behavior would be Undefined on that basis alone.

The one thing that gets tricky is that under a strict reading of the Standard, even something so straightforward as

int main(void)
{
  struct { int x; } foo;
  foo.x = 1;
  return 0;
}

also violates N1570 6.5p7 because foo.x is an lvalue of type int, it is used to access the stored value of an object of type struct foo, and type int does not satisfy any of the conditions on that section.

The only way the Standard can be even remotely useful is if one recognizes that there need to be exceptions to N1570 6.5p7 in cases involving lvalues that are derived from other lvalues. If the Standard were to describe cases where compilers may or must recognize such derivation, and specify that N1570 6.5p7 only applies in cases where storage is accessed using more than one type within a particular execution of a function or loop, that would have eliminated a lot of complexity including any need for the notion of "Effective Type".

Unfortunately, some compilers have taken it upon themselves to ignore derivation of lvalues and pointers even in some obvious cases like:

s1 *p1 = &unionArr[i].v1;
p1->x ++;

It may be reasonable for a compiler to fail to recognize the association between p1 and unionArr[i].v1 if other actions involving unionArr[i] separated the creation and use of p1, but neither gcc nor clang can consistently recognize such association even in simple cases where the use of the pointer immediately follows the action which takes the address of the union member.

Again, since the Standard doesn't require that compilers recognize any usage of derived lvalues unless they are of character types, the behavior of gcc and clang does not make them non-conforming. On the other hand, the only reason they are conforming is because of a defect in the Standard which is so outrageous that nobody reads the Standard as saying what it actually does.

Padget answered 15/3, 2018 at 0:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.