Ike said:
[ about printf("%d", ++a, a + 5); ]
'++a' and 'a + 5' are separate expressions.
The value of 'a' is intact until the sequence point.
It looks like you are mistaken about the nature of sequence points.
A sequence point is a point in time where nothing happens,
a stability point, a point of rest. It is _not_ a point of action.
Transitions happen _between_ sequence points; at the sequence point
itself, the machine for a moment has come to a halt, all transitions
that happened before the sequence point have completed, and all
transitions that will happen after the sequence point have not yet started.
Ok. After Mr. K. Thompson's response, it was clear that this claim was
incomplete. The treatment was: Reading non-volatile objects' values
isn't a side effect, but can be required to accomplish evaluation. It
seemed natural that all writes should pend for _just_before_ the
sequence point (after evaluations are complete), thus allowing for
non-volatile reads to have ensured consistency for the duration of
evaluation. That is: Previous sequence point 1-> All reads and
computations 2-> All writes 3-> Next sequence point.
But that's not the case. Though I meant 3-> by "until", it's all
invention unless established by agreement beforehand... Which it's not
at all.
We could draw a timeline, like this:
activity rest activity rest activity
---------------+--------------------+--------------
seq seq
From what you write, it looks like you think that all the action
happens _at_ sequence points (sorry if I'm understanding you wrong).
Well yes, "at" as "just before, but after all evaluations have been
completed," and only in as much as the context of the write to 'a'. But
C is not so strict. Side effects just happen at random between sequence
points, as far as analysis with only a C Standard and not the
implementation's specifics goes, right?
In `` printf("%d", ++a, a + 5); '' the first sequence point, S0, is
before the statement starts. The next sequence point, S1, is before the
call to printf. In between those sequence points, the arguments
(the expressions ``"%d"'', ``++a'' and ``a+5'') are evaluated.
Several accesses to ``a'' will happen here: the ``a'' in ``++a''
will be read to obtain its previous value (event ar0),
the ``a'' in ``++a'' will be modified to store
the incremented value (event aw0) and ``a'' in ``a+5'' will be
read to compute the sum of ``a'' and ``5'' (event ar1).
Yes, an excellent explanation of the goings-ons, thanks.
Necessarily, ar0 must happen before aw0, because the value written
at aw0 depends on the value read at ar0. But ar1 can happen at any
time, so, the following interleavings are possible:
ar1,ar0,aw0
--+---------------+--
S0 S1
ar0,ar1,aw0
--+---------------+--
S0 S1
ar0,aw0,ar1
--+---------------+--
S0 S1
That's why the value of the third argument ``a+5'' is not well-defined.
It depends on the order in which aw0 and ar1 occur, and any order is
possible.
Right. But why not "unspecified" rather than "undefined behaviour"?
Isn't it easy to get unspecified program results which are not
undefined, but still useful based on knowledge of the implementation, or
still portable in that a program does not crash and is guaranteed to
translate?
In standardese: the variable ``a'' is both read and modified
between two sequence points, and the value is read (at ar1) for another
purpose than to compute the value written (at aw0).
....And all within evaluation of an expression (the function call),
apparently.
When the write does not depend on the read, the implementation has
the freedom to schedule the read and the write in any order it wishes.
Sure.
Here are three chunks about "Expressions", from C99's section 6.5:
"1 An expression is a sequence of operators and operands that specifies
computation of a value, or that designates an object or a function, or
that generates side effects, or that performs a combination thereof.
"2 Between the previous and next sequence point an object shall have its
stored value modified at most once by the evaluation of an
expression.72) Furthermore, the prior value shall be read only to
determine the value to be stored.73)
"3 The grouping of operators and operands is indicated by the syntax.74)
Except as specified later (for the function-call (), &&, ||, ?:, and
comma operators), the order of evaluation of subexpressions and the
order in which side effects take place are both unspecified."
So what we must accept is:
- At least one of:
- Each function call "argument" is an "operand" and the function call
is an N-ary operator.
- An optional argument-expression-list is an "operand" and the
postfix-expression for the denoting the called function is the other
"operand".
- Zero or more commas within the argument-expression-list are part of
the function call operator and not a syntactic means of creating
expressions disjoint from the whole function call expression.
- Having a "previous" and "next" sequence point does not imply a linear
(even if unspecified or specified) ordering of object modifications and
reads.
- The evaluation of an expression endures the evaluation of all
sub-expressions.
- Any expression which reads or modifies objects and which appears
within a syntactically encompassing expression can be said to contribute
those attributes to the larger expression.
- The first "shall" in p2 is not a constraint on implementation
conformance, but a constraint on source code. That is, it's not a
constraint which means, "a conforming implementation shall ensure that
expression evaluation does not result in multiple modifications of an
object between sequence points," but means, "the programmer shall not
design an expression which appears to modify an object multiple times
between sequence points."
- The second "shall" in p2 is not a constraint on implementation
conformance, but a constraint on source code. That is, it's not a
constraint which means, "a conforming implementation shall ensure that
an expression and all sub-expressions do not yield any reads of an
object outside of using those values to determine a value to modify that
object with, if and only if that object is modified," but means, "the
programmer shall not design an expression which contains a
sub-expression which modifies an object, but also contains other
sub-expressions outside of that sub-expression which read the object."
- p2 does not prevent the read of an object from contributing to not
just a new value for the object, but even towards computations for
syntactically encompassing expressions. [(i = j = j + 1)] [(i = (j = j
+ 1) + 1)] (But wait a minute...)
- p3 does not call attention to the function call operator to
distinguish it from _all_ other operators, but because it has a sequence
point just as the other noted operators do, and that group also has some
detail about what comes before the sequence point and what comes after.
- Whereas type-name and assignment-operator in the syntax are not by
themselves a sub-expression, the optional argument-expression-list and
any included, syntactic commas do constitute something which is
considered part of the evaluation of the expression containing the
function call.
- The list of expressions in "Function calls", section 6.5.2.2, which
constitute the arguments with their unspecified evaluation order, are to
be regarded as sub-expressions of the whole function call expression; a
modification in one is a modification in the whole.
Ok. Isn't it interesting how "any order" for the function calls in
example 6.5.2.2p12 means "one at a time" but we are allowed simultaneous
order for the write of '++a' and the read of 'a + 5'? Or does it?
Isn't it interesting that 6.5.2.2p10 details the unspecified order for
"the function designator, the actual arguments, and subexpressions
within the actual arguments." Why the distinction on "subexpressions"
here? Why not the distinction in 6.5p2?
It's interesting that the Standard grants a license (optimization iff
congruent results to abstract semantics) to the implementation, then the
implementation returns the favour (specify anything other than a linear
order for evaluation, because I might optimize using concurrent
evaluations).
Even in such a case, rather than making 6.5p2 violations unspecified
behaviour, it's outright undefined behaviour... Meaning an
implementation can refuse to translate, or can accidentally optimize a
simultaneous read and write to the same memory, rather than making an
arbitrary choice for a linear order, and landing the original post at
answer (b). Very well!