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.

melpomene
  • 84,125
  • 8
  • 85
  • 148
Vroomfondel
  • 2,704
  • 1
  • 15
  • 29

4 Answers4

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.

haccks
  • 104,019
  • 25
  • 176
  • 264
  • That's not quite true: `printf("%d", i + i++);` has undefined behavior. – melpomene Oct 22 '15 at 21:36
  • 1
    An unsequenced read and write of the same memory location is UB just like two unsequenced writes. – Brian Bi Oct 22 '15 at 21:37
  • Your example contains an unsequenced read/write (or what do you mean by "unsequenced"?). – melpomene Oct 22 '15 at 21:39
  • Read the reference added to the answer. – haccks Oct 22 '15 at 21:41
  • @haccks That still doesn't explain what "unsequenced" means. – melpomene Oct 22 '15 at 21:43
  • @melpomene; What makes it difficult to understand the meaning of "unsequenced" in this context? – haccks Oct 22 '15 at 21:46
  • @haccks The lack of a definition. – melpomene Oct 22 '15 at 21:46
  • @melpomene; Where you find difficulties? I will try to explain. – haccks Oct 22 '15 at 21:48
  • @haccks 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 ...*" – melpomene Oct 22 '15 at 21:49
  • 3
    @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. – John Bollinger Oct 22 '15 at 21:50
  • @melpomene; What do you mean by *schnitzelkraut*? – haccks Oct 22 '15 at 21:51
  • @haccks I have no idea! That's why I keep asking you for a definition! – melpomene Oct 22 '15 at 21:52
  • @JohnBollinger Will you buy me a copy of the standard? – melpomene Oct 22 '15 at 21:53
  • 3
    @melpomene; Read [this](http://stackoverflow.com/a/31083924/2455888) to know everything about *sequence before* and *unsequenced*. Download n1570 pdf from [here](http://www.compsci.hunter.cuny.edu/~sweiss/resources/c11standard.pdf). – haccks Oct 22 '15 at 21:56
  • @haccks That answer defines *unsequenced*, but not *sequenced*. – melpomene Oct 22 '15 at 22:03
  • 2
    @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. – John Bollinger Oct 22 '15 at 22:03
  • @melpomene; That defines *sequenced*. – haccks Oct 22 '15 at 22:05
  • @JohnBollinger; Thanks for the reference. I should have mentioned that but I thought that it is obvious. – haccks Oct 22 '15 at 22:11
  • 1
    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.) – supercat Oct 30 '15 at 20:48
  • @supercat 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! – alx - recommends codidact Jun 17 '22 at 07:08
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.

Columbo
  • 60,038
  • 8
  • 155
  • 203
  • @Barry It means the latter. See CWG 556. – Columbo Oct 22 '15 at 21:57
  • Currently I do not have C++ standard copy to check for more detail but I have some doubt on your your explanation. – haccks Oct 22 '15 at 21:58
  • 1
    @Columbo But then that's weird right? `u.a = u.b` is undefined, but `u.a = B(u.b)` is fine? – Barry Oct 22 '15 at 21:59
  • @Barry 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.foo`s. 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. – Columbo Oct 22 '15 at 22:06
  • 1
    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++. – John Bollinger Oct 22 '15 at 22:09
  • @JohnBollinger It doesn't. What are you referring to? – Columbo Oct 22 '15 at 22:11
  • @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. – John Bollinger Oct 22 '15 at 22:15
  • @JohnBollinger Sorry for confusing everyone with sloppy language-lawyering. :-) – Columbo Oct 22 '15 at 22:19
  • "_object whose lifetime hasn't begun yet - baz.bar_" what? – curiousguy Oct 28 '15 at 12:55
  • @curiousguy `bar` is not alive in the snippet of the asker. – Columbo Oct 28 '15 at 13:44
  • @Columbo Do you mean not initialized? – curiousguy Oct 28 '15 at 13:46
  • @curiousguy …effectively. I'd familiarize myself with standard terminology, though. – Columbo Oct 28 '15 at 15:06
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().

  • 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. – alx - recommends codidact Jun 17 '22 at 06:30
  • And that also triggers the question: Is `ub1.a = ub1.b + 0;` defined?! – alx - recommends codidact Jun 17 '22 at 07:04
  • 1
    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. – supercat Jun 17 '22 at 15:00
  • 1
    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. – supercat Jun 17 '22 at 17:57
  • 1
    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. – Vroomfondel Jun 20 '22 at 10:17
  • @supercat : 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. – Vroomfondel Jun 20 '22 at 11:32
  • @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... – supercat Jun 20 '22 at 14:52
  • ...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). – supercat Jun 20 '22 at 14:54
  • @supercat 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. – Vroomfondel Jun 21 '22 at 08:23
  • @Vroomfondel 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? – alx - recommends codidact Jun 21 '22 at 11:55
  • 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. – alx - recommends codidact Jun 21 '22 at 12:00
  • 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. – Vroomfondel Jun 21 '22 at 12:15
  • @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). – supercat Jun 21 '22 at 14:45
  • @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`. – supercat Jun 21 '22 at 14:50
  • @Vroomfondel Okay, would you mind writing a full answer detailing that? – alx - recommends codidact Jun 21 '22 at 15:07
  • Or @supercat, SO should allow mentioning 2 users... – alx - recommends codidact Jun 21 '22 at 15:08
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.

supercat
  • 77,689
  • 9
  • 166
  • 211