C/C++ unions and undefined behaviour
Asked Answered
B

4

8

Is the following undefined behaviour?

 union {
   int foo;
   float bar;
 } baz;

 baz.foo = 3.14 * baz.bar;

I remember that writing and reading from the same underlying memory between two sequence points is UB, but I am not certain.

Blois answered 22/10, 2015 at 21:31 Comment(5)
Evaluation is unordered, but not the side effects, which are ordered in C++11.Marybellemarybeth
Which language do you want an answer for?Ferdy
@AlanStokes: C and C++, as tagged :DBlois
Please be clear whether you want a C or C++ answer. Not both, for otherwise this question is too broad.Glottochronology
@Glottochronology Why does the SO allow both tags then?Ailey
O
6

I remember that writing and reading from the same underlying memory between two sequence points is UB, but I am not certain.

Reading and writing to the same memory location in the same expression does not invoke undefined behavior until and unless that location is modified more than once between two sequence points or the side effect is unsequenced relative to the value computation using the value at the same location.

C11: 6.5 Expressions:

If a side effect on a scalar object is unsequenced relative to either a different side effect on the same scalar object or a value computation using the value of the same scalar object, the behavior is undefined. [...]

The expression

 baz.foo = 3.14 * baz.bar;  

has well defined behaviour if bar is initialized before. The reason is that the side effect to baz.foo is sequenced relative to the value computations of the objects baz.foo and baz.bar.

C11: 6.5.16/3 Assignment operators:

[...] The side effect of updating the stored value of the left operand is sequenced after the value computations of the left and right operands. The evaluations of the operands are unsequenced.

Overtone answered 22/10, 2015 at 21:36 Comment(20)
That's not quite true: printf("%d", i + i++); has undefined behavior.Turrell
An unsequenced read and write of the same memory location is UB just like two unsequenced writes.Noland
Your example contains an unsequenced read/write (or what do you mean by "unsequenced"?).Turrell
Read the reference added to the answer.Overtone
@Overtone That still doesn't explain what "unsequenced" means.Turrell
@melpomene; What makes it difficult to understand the meaning of "unsequenced" in this context?Overtone
@Overtone The lack of a definition.Turrell
@melpomene; Where you find difficulties? I will try to explain.Overtone
@Overtone I have literally no idea what you mean by "unsequenced". You might as well have said "If a side effect on a scalar object is schnitzelkraut relative to either a different side effect ..."Turrell
@melpomene, "unsequenced" is a defined term in the C2011 standard. A complete definition would probably be inappropriately large for this venue, but I encourage you to read what the standard itself says about it, and about the "sequenced before" relation.Cerous
@melpomene; What do you mean by schnitzelkraut?Overtone
@Overtone I have no idea! That's why I keep asking you for a definition!Turrell
@JohnBollinger Will you buy me a copy of the standard?Turrell
@melpomene; Read this to know everything about sequence before and unsequenced. Download n1570 pdf from here.Overtone
@Overtone That answer defines unsequenced, but not sequenced.Turrell
@haccks, you've omitted mention of the key provision of the standard relevant to this question: "The side effect of updating the stored value of the left operand [of an assignment operator] is sequenced after the value computations of the left and right operands" (from C11 6.5.16/3). Absent that, or some other provision having the same effect, the provision you quoted would hold that the behavior is undefined.Cerous
@melpomene; That defines sequenced.Overtone
@JohnBollinger; Thanks for the reference. I should have mentioned that but I thought that it is obvious.Overtone
I think historically there was a requirement that an object may only be read and written in the same expression if both were accessed the same way. Certainly there are many machines where such a rule would enable useful optimizations (e.g. on an 8x51 clone with two data pointers, given "uint32_t *foo,*bar;" copying four bytes from "foo" to "bar" would be most efficiently implemented by copying the first byte of foo to the first byte of bar, then the second byte, third, and fourth, but that could malfunction if they overlap.)Ruthenium
@Ruthenium I could find what you say in the C2x draft, so it seems it's not a historical thingy. However, I'm not sure I understand all the implications of the text as written in the standard stones. Could you please review my answer, and especially the comments I wrote after it? Thanks!Commuter
S
4

Disclaimer: This answer addresses C++.

You're accessing an object whose lifetime hasn't begun yet - baz.bar - which induces UB by [basic.life]/(6.1).

Assuming bar has been brought to life (e.g. by initializing it), your code is fine; before the assignment, foo need not be alive as no operation is performed that depends on its value, and during it, the active member is changed by reusing the memory and effectively initializing it. The current rules aren't clear about the latter; see CWG #1116. However, the status quo is that such assignments are indeed setting the target member as active (=alive).

Note that the assignment is sequenced (i.e. guaranteed to happen) after the value computation of the operands - see [expr.ass]/1.

Skullcap answered 22/10, 2015 at 21:48 Comment(12)
@Barry It means the latter. See CWG 556.Skullcap
Currently I do not have C++ standard copy to check for more detail but I have some doubt on your your explanation.Overtone
@Skullcap But then that's weird right? u.a = u.b is undefined, but u.a = B(u.b) is fine?Endpaper
@Endpaper It is weird, but it's the intention of the wording AFAICS ("instead of being a general statement about aliasing, it's describing the situation in which the source of the value being assigned is storage that overlaps the storage of the target object"). The target object is a temporary of type float, but that temporary's storage does certainly not overlap baz.foos. Then again, perhaps the committee was not precise enough in wording their note, and they actually did mean that u.a=f(u.b) is not defined. Eitherway, lifetime rules are a mess.Skullcap
The behavior is definitely defined in C, supposing baz.bar has been initialized and baz.foo has not subsequently been written to. Given the C / C++ reconciliation efforts in the 2011 versions of the standards, I would be very surprised to find that the same code has undefined behavior in C++.Cerous
@JohnBollinger It doesn't. What are you referring to?Skullcap
@Columbo, I'm referring to the earlier version of your answer, which said the opposite of what your answer now says. I hadn't yet received the update. Sorry for the noise.Cerous
@JohnBollinger Sorry for confusing everyone with sloppy language-lawyering. :-)Skullcap
"object whose lifetime hasn't begun yet - baz.bar" what?Ailey
@Ailey bar is not alive in the snippet of the asker.Skullcap
@Skullcap Do you mean not initialized?Ailey
@Ailey …effectively. I'd familiarize myself with standard terminology, though.Skullcap
C
2

Answering for C, not C++

I thought this was Defined Behavior, but then I read the following paragraph from ISO C2x (which I guess is also present in older C standards, but didn't check):

6.5.16.1/3 (Assignment operators::Simple Assignment::Semantics):

If the value being stored in an object is read from another object that overlaps in any way the storage of the first object, then the overlap shall be exact and the two objects shall have qualified or unqualified versions of a compatible type; otherwise, the behavior is undefined.

So, let's consider the following:

union {
    int        a;
    const int  b;
} db;

union {
    int    a;
    float  b;
} ub1;

union {
    uint32_t  a;
    int32_t   b;
} ub2;

Then, it is Defined Behavior to do:

db.a = db.b + 1;

But it is Undefined Behavior to do:

ub1.a = ub1.b + 1;

or

ub2.a = ub2.b + 1;

The definition of compatible types is in 6.2.7/1 (Compatible type and composite type). See also: __builtin_types_compatible_p().

Commuter answered 16/6, 2022 at 22:14 Comment(16)
Although, does + create a new object such that makes it defined again? I.e., ub1.a = ub1.b; is undefined behavior for sure, but is it ub1.a = ub1.b + 1;? I got a warning from that code, but I'm not convinced.Commuter
And that also triggers the question: Is ub1.a = ub1.b + 0; defined?!Commuter
So far as I am aware, nothing in the Standard implies that the built-in operators would produce copies of their operands, and on many 8-bit or 16-bit platforms, having long1 = long2 + 1; perform an intermediate copy would substantially increase code size and execution time.Ruthenium
Incidentally, on some 8-bit platforms, the fastest way to perform long1 = long2+long3; is to process the code as though it were long1 = long2; long1+=long3; or long1 = long3; long1+=long2;, but the first substitution is only valid if long1 and long3 are known to identify disjoint regions of storage, and the second is only valid if long1 and long2 are distinct.Ruthenium
I upvoted for the obvious effort that went into the answer - but I think it is wrong. ub1.a and ub1.b are not alive at the same time, so there are no two objects which overlap, which makes the clause not applicable. Together with 6.5.16/3 (side effect of updating after read) this seems to make all three cases legal.Blois
@Ruthenium : I see no problem with your examples. The rhs objects undergo lvalue conversion (6.3.2.1/2) and if they overlap, they satisfy the clause mentioned in the answer (6.5.16.1/3). Your command substitution are immaterial for the standard, and if the compiler can tell that those are restrict or somehow else different objects, it will use your optimized version, otherwise it has to go the whole route with a temporary.Blois
@Vroomfondel: The intention of the Standard is that compilers do whatever is necessary handle cases where the source and destination of an assignment share storage in particular ways recognized by the Standard, but not that they be required to allow for the possibility that objects may share storage in ways a compiler would have no particular reason to expect. One thing people lose sight of is that the Standard was designed to allow compiler writers to make their products as useful as possible. On some platforms, it may be expensive to uphold behavioral guarantees that would be...Ruthenium
...commonplace on other platforms, and the intention of the Standard was to allow "non-portable" programs to exploit such guarantees, but allow the writers for obscure platforms to judge whether it would be more useful to behave in commonplace fashion or do something else that was more useful (e.g. by virtue of being faster or needing less machine code).Ruthenium
@Ruthenium all agreed, but I didn't say anything againts that, did I? So all of your assignments can be translated to the optimized version, unless there is insufficient knowledge to do so.Blois
@Blois so then in which cases does that paragraph apply? Assigning *ap = *bp from two non-restrict pointers I guess (or also one pointer and the variable it points to a = *ap)? I can't think of any other valid cases. But in those cases, aliasing rules already prevent that, unless using char, but char aliasing is safe because of sizeof(char) == 1 (and because the standard says so), so how could this paragraph in the standard be useful without referring to unions?Commuter
When assigning *ap = *bp, if both pointers have the same type, then this paragraph doesn't apply, because obviously they have the same size and starting location, so aliasing rules are already enough. If they don't have the same type, then it's breaking aliasing rules.Commuter
I reckon the use case is reinterpreting data as valid object of a struct x{}; albeit it really was written via another struct y{}; which by this paragraph has to be identical enough for C to not cause representation problems.Blois
@alx: The aliasing rules would not be enough in the hardly-uncommon case that one of the references is of character type. For example, to process something like x+=(*p) << 4; on some PIC platforms, it may be most efficient on some 8-bit platforms to load *p, shift left by four, add with carry to the bottom half of x, reload *p, shift right by four, and add to the top half of x (the main cost of loading *p would be setting up the pointer, which would only need to be done once).Ruthenium
@alx: E.g. 8051: once pointer address is in R0, mov a,@r0 / swap / and a,#$F0 / add a,x.lo / mov x.low,a / mov a,@r0 / swap / and a,$0F / adc x.hi / mov x.hi,a. All well and good unless p might alias the bottom half of x.Ruthenium
@Blois Okay, would you mind writing a full answer detailing that?Commuter
Or @supercat, SO should allow mentioning 2 users...Commuter
R
0

The Standard uses the phrase "Undefined Behavior", among other things, as a catch-all for situations where many implementations would process a construct in at least somewhat predictable fashion (e.g. yielding a not-necessarily-predictable value without side effects), but where the authors of the Standard thought it impractical to try to anticipate everything that implementations might do. It wasn't intended as an invitation for implementations to behave gratuitously nonsensically, nor as an indication that code was erroneous (the phrase "non-portable or erroneous" was very much intended to include constructs that might fail on some machines, but would be correct on code which was not intended to be suitable for use with those machines).

On some platforms like the 8051, if a compiler were given a construct like someInt16 += *someUnsignedCharPtr << 4; the most efficient way to process it if it didn't have to accommodate the possibility that the pointer might point to the lower byte of someInt16 would be to fetch *someUnsignedCharPtr, shift it left four bits, add it to the LSB of someInt16 (capturing the carry), reload *someUnsignedCharPtr, shift it right four bits, and add it along with the earlier carry to the MSB of someInt16. Loading the value from *someUnsignedCharPtr twice would be faster than loading it, storing its value to a temporary location before doing the shift, and then having to load its value from that temporary location. If, however, someUnsignedCharPtr were to point to the lower byte of someInt16, then the modification of that lower byte before the second load of someUnsignedCharPtr would corrupt the upper bits of that byte which would, after shifing, be added to the upper byte of someInt16.

The Standard would allow a compiler to generate such code, even though character pointers are exempt from aliasing rules, because it does not require that compilers handle all situations where unsequenced reads and writes affect regions of storage that partially overlap. If such accesses were performed usinng a union instead of a character pointer, a compiler might recognize that the character-type access would always overlap the least significant byte of the 16-bit value, but I don't think the authors of the Standard wanted to require that compilers invest the time and effort that might be necessary to handle such obscure cases meaningfully.

Ruthenium answered 21/6, 2022 at 18:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.