I think my book (Programming Languages: Principles and Paradigms) is wrong. a
is a vector, assume a C-like language:
b = 0;
a[f(3)] = a[f(3)] + 1;
int f(int n) {
if(b == 0) {
b = 1;
return 1;
}
else return 2;
}
It has the effect of assigning the value of
a[1] + 1
toa[2]
whenever
the evaluation of the left-hand component of the assignment precedes
the evaluation of the right-hand one.
For me, it has the opposite effect, that is assigning the value of a[2] + 1
to a[1]
.
Because the left-hand component is evaluated first, so f
returns 1
(because b
is 0
), thus a[1]
. Then the right-hand component is evaluated, f
returns 2
(because now b
is 1
), thus a[2] + 1
.
Am I right and the book is wrong? Unfortunatly there is no errata…
7
You wrote “assume a C-like language”. Just how C-like should it be?
First, it does appear that the author’s logic is backwards:
It has the effect of assigning the value of a[1] + 1 to a[2] whenever
the evaluation of the left-hand component of the assignment precedes
the evaluation of the right-hand one.
In fact, it has that effect if the evaluation of the LHS follows the evaluation of the RHS.
Some “C-like” languages (in the sense that their syntax looks similar to C’s) define the order of evaluation of expressions. (I think Java does this.) In such a language, the semantics of the code would depend on exactly how the language defines the order of evaluation. If the order is left-to-right, then the function call on the LHS (left-hand side) of the assignment will be evaluated first.
In C itself (and in C++), the order of evaluation is unspecified. The two function calls could be evaluated in either order, depending on the whim of the compiler (optimization can change this; so, in principle, can the phase of the moon). I don’t believe the behavior of this particular code is actually undefined, which would mean that the language says nothing about how it behaves, but similar code such as:
a[i++] = a[i++] + 1;
does have undefined behavior, because i
is modified twice with no intervening sequence point. In C, the above could evaluate the LHS i++
before the right one, or vice versa — but those aren’t the only possibilities. A C compiler is free to assume that your program doesn’t modify i
twice between sequence points. This particular example seems easy for the compiler to detect, but in more complex cases it can be difficult or impossible to do so.
But the real answer to the question is: Don’t write code like that.
You really don’t need to have two function calls in a single statement, where the results depend on the order in which they’re evaluated. Separate the calls into distinct statements, for example:
int x = f(3);
int y = f(3);
a[x] = a[y] + 1;
And in real life, you want better names than x
, y
, f
, and a
— and f()
probably shouldn’t return different results on successive calls, or it should be very clear why it needs to do so.
Write your code so that questions like this don’t arise in the first place.
4
The order of evaluation is usually undefined, and therefore can go either way depending on the compiler, although some pedantic languages might define it. That’s why most of the time when you see an example like that, it’s to warn you not to do it.
If the compiler goes strictly by the order the index is needed, the book is right, because you have to read the value, then perform the addition, then do the assignment. However, you don’t have to compute the array indexes in the same order they are used, and some optimizations might take advantage of that fact.
Allow me to demonstrate with some intermediate representation pseudocode. The first pass of a compiler will usually produce something like this:
temp1 = f(3)
temp2 = a[temp1]
temp3 = temp2 + 2
temp4 = f(3)
a[temp4] = temp3
Line 1 must come before line 2. Line 2 must come before line 3. Line 3 must come before line 5. Line 4 must come before line 5. Aside from that, the order of execution is not usually defined by the language, so compilers will do whatever is more efficient or easier to implement. It is perfectly allowed to end up like this:
temp4 = f(3)
temp1 = f(3)
temp2 = a[temp1]
temp3 = temp2 + 2
a[temp4] = temp3
A compiler might do it that way if the last three lines can be combined into a single assembly instruction. Since the destination is often specified first in an assembly instruction, it might make sense to calculate the destination index first, but you don’t have to do it in that order.
Another compiler might not have so many registers to work with, or have a smaller instruction set, so they might calculate the source index first in order to be able to reuse the temp1
register.
Languages usually purposely leave it undefined to allow compiler implementations to do whatever is most efficient.
3
I think your interpretation is correct. If the book’s author has an e-mail address, you might want to contact him.
I have not read the book, but I guess the next paragraph will state that it is dangerous to rely on such side-effects, since they make code hard to read and lead to difficult bugs. Quod erat demonstrandum, I would say. 🙂
Your line of code is equivalent to
a[x] = a [y] + 1;
The book may have it backwards, saying something happens when the left (x) is evalulated first, but actually that happens when the right (y) is evaluated first, but that is almost certainly not the point.
The point is that you don’t know whether x or y will be evaluated first. You may feel that “before computing the value (right-hand side), the compiler should compute the l-value (left-side)” but you didn’t write every compiler on the planet. Some compiler writers might want to go left to right, some right to left, some according to the order you will need them – you need y first, to get a value to add 1 to, and only then do you need x to figure out where to put the value – and they are all correct because this particular decision is left up to the compiler writer and you must not rely on it, ever.
1
You are correct. The first call to f() will evaluate to 1, subsequent ones evaluate to 2. If the left-hand operand of the assignment is indeed fully evaluated first (which is kind of debatable), then the assignment evaluates to:
a[1] = a[2] + 1;
This, however, also assumes that the value of b
is never modified before this line is called, and also that f()
is never called before this line.
Also note that the n
parameter to f()
is never used, so I assume it’s quite likely that there is some typo of sorts there.
It would be interesting to know what point the author is trying to make here.
2