Take the 2-minute tour ×
Programmers Stack Exchange is a question and answer site for professional programmers interested in conceptual questions about software development. It's 100% free, no registration required.

It's undefined because the it modifies x twice between sequence points. The standard says it's undefined, therefore it's undefined.
That much I know.

But why?

My understanding is that forbidding this allows compilers to optimize better. This could have made sense when C was invented, but now seems like a weak argument.
If we were to reinvent C today, would we do it this way, or can it be done better?
Or maybe there's a deeper problem, that makes it hard to define consistent rules for such expressions, so it's best to forbid them?

So suppose we were to reinvent C today. I'd like to suggest simple rules for expressions such as x=x++, which seem to me to work better than the existing rules.
I'd like to get your opinion on the suggested rules compared to the existing ones, or other suggestions.

Suggested Rules:

  1. Between sequence points, order of evaluation is unspecified.
  2. Side effects take place immediately.

There's no undefined behavior involved. Expressions evaluate to this value or that, but surely won't format your hard disk (strangely, I've never seen an implementation where x=x++ formats the hard disk).

Example Expressions

  1. x=x++ - Well defined, doesn't change x.
    First, x is incremented (immediately when x++ is evaluated), then it's old value is stored in x.

  2. x++ + ++x - Increments x twice, evaluates to 2*x+2.
    Though either side may be evaluated first, the result is either x + (x+2) (left side first) or (x+1) + (x+1) (right side first).

  3. x = x + (x=3) - Unspecified, x set to either x+3 or 6.
    If the right side is evaluated first, it's x+3. It's also possible that x=3 is evaluated first, so it's 3+3. In either case, the x=3 assignment happens immediately when x=3 is evaluated, so the value stored is overwritten by the other assignment.

  4. x+=(x=3) - Well defined, sets x to 6.
    You could argue that this is just shorthand for the expression above.
    But I'd say that += must be executed after x=3, and not in two parts (read x, evaluate x=3, add and store new value).

What's the Advantage?

Some comments raised this good point.
I certainly don't think expressions such as x=x++ should be used in any normal code.
Actually, I'm much more strict than that - I think the only good usage for x++ in as x++; alone.

However, I think the language rules must be as simple as possible. Otherwise programmers just don't understand them. the rule forbidding changing a variable twice between sequence points is certainly a rule most programmers don't understand.

A very basic rule is this:
If A is valid, and B is valid, and they're combined in a valid way, the result is valid.
x is a valid L-value, x++ is a valid expression, and = is a valid way to combine an L-value and an expression, so how come x=x++ isn't legal?
The C standard makes an exception here, and this exception complicates the rules. You can search stackoverflow.com and see how much this exception confuses people.
So I say - get rid of this confusion.

=== Summary of Answers ===

  1. Why do that?
    I tried to explain in the section above - I want C rules to be simple.

  2. Potential for optimization:
    This does take some freedom from the compiler, but I didn't see anything that convinced me that it might be significant.
    Most optimizations can still be done. For example, a=3;b=5; can be reordered, even though the standard specifies the order. Expressions such as a=b[i++] can still be optimized similarly.

  3. You can't change the existing standard.
    I admit, I can't. I never thought I can actually go ahead and change standards and compilers. I only wanted to think if things could have been done differently.

share|improve this question
9  
Why is this important to you? Should it be defined, and if so, why? There's not much point in assigning x to itself, and if you want to increment x you can just say x++; -- no need for the assignment. I'd say it should not be defined just because it'd be hard to remember what's supposed to happen. –  Caleb Jun 19 '12 at 7:43
4  
In my mind, this is a good question ("Some men see things as they are and ask why, I dream things that never were and ask why not"). It's (in my opinion) a question purely on language design, using C syntax as an example, not a question on C syntax. I, personally, think that the reason we don't have defined behaviour for expressions such as x++ + ++x or x=x++ is simply because there is a possibility of them being misread. –  Jamie Taylor Jun 19 '12 at 8:06
5  
@ugoren: Why do you need to predict the result. Nobody in their right mind would write code like that (as has been mentioned several times) even if you did write code like this it would be rejected at the first code review. So there is no need to define the behavior and give the optimizer the best chance at optimizing it. In every example you propose I would shoot somebody if they added that to the code base. –  Loki Astari Jun 19 '12 at 8:13
3  
I would find a more interesting question to be why isn't an error to write this? Surely a compiler could detect it's undefined behavour and therefore can't possibly be what the user actually wanted, so why isn't in an error? I understand some instances of undefined bahavour are hard to detect but this isn;t –  JohnB Jun 19 '12 at 12:27
3  
"the rule forbidding changing a variable twice between sequence points is certainly a rule most programmers don't understand." -- Do you have any evidence for this? Most questions I've seen were asked because the programmers didn't know about the rule. Is there any evidence that most of them still didn't understand it after it was explained? –  Secure Jun 19 '12 at 15:44

8 Answers 8

Maybe you should first answer the question why it should be defined? Is there any advantage in programming style, readability, maintainability or performance by allowing such expressions with additional side effects? Is

y = x++ + ++x;

more readable than

y = 2*x + 2;
x += 2;

Given that such a change is extremely fundamental and breaking to the existing code base.

share|improve this answer
1  
I added a "why" section to my question. I surely don't suggest using these expressions, but I'm interested in having simple rules to tell the meaning of an expression. –  ugoren Jun 19 '12 at 8:00
    
Also, this change doesn't break existing code, unless it invoked undefined behavior. Correct me if I'm wrong. –  ugoren Jun 19 '12 at 8:01
3  
Well, a more philosophical answer: It's currently undefined. If no programmer uses it, then there is no need for you to understand such expressions, because there shouldn't be any code. If there is a need for you to understand them, then obviously there must be a lot of code out there that relies on undefined behaviour. ;) –  Secure Jun 19 '12 at 8:15
1  
It is, by definition, not breaking any existing codebase to define the behaviours. If they contained UB, they were, by definition, already broken. –  DeadMG Jun 19 '12 at 12:29
1  
@ugoren : Your "why" section still doesn't answer the practical question: why would you want this freakish expression in your code? If you can't come up with a convincing answer to that, then the whole discussion is moot. –  Mike Baranczak Jun 19 '12 at 13:14

The argument that making this undefined behavior allows better optimization is not weak today. In fact, it's much stronger today than it was when C was new.

When C was new, machines that could take advantage of this for better optimization were mostly theoretical models. People had talked about the possibility of building CPUs where the compiler would instruct the CPU about what instructions could/should be executed in parallel with other instructions. They pointed to the fact that allowing this to have undefined behavior meant that on such a CPU, if it ever really existed, you could schedule the "increment" part of the instruction to execute in parallel with the rest of the instruction stream. While they were right about the theory, at the time there was little in the way of hardware that could really take advantage of this possibility.

That's not just theoretical anymore. Now there is hardware in production, and in wide use, (e.g., Itanium, VLIW DSPs) that can really take advantage of this. They really do allow the compiler to generate an instruction stream that specifies that instructions X, Y and Z can all be executed in parallel. This is no longer a theoretical model -- it's real hardware in real use doing real work.

IMO, making this defined behavior is close to the worst possible "solution" to the problem. You clearly should not use expressions like this. For the vast majority of code, the ideal behavior would be for the compiler to simply reject such expressions entirely. At the time, C compilers didn't do the flow analysis necessary to detect that dependably. Even at the time of the original C standard, it still wasn't at all common.

I'm not sure it would be acceptable to the community today either -- while many compilers can do that kind of flow analysis, they typically only do it when you request optimization. I doubt most programmers would like the idea of slowing down "debug" builds just for the sake of being able to reject code they (being sane) would never write in the first place.

What C has done is a semi-reasonable second-best choice: tell people not to do that, allowing (but not requiring) the compiler to reject the code. This avoids (still further) slowing compilation for people who'd never use it, but still allows somebody to write a compiler that will reject such code if they want to (and/or have flags that will reject it that people can choose to use or not as they see fit).

At least IMO, making this defined behavior would be (at least close to) the worst possible decision to make. On VLIW-style hardware, you choices would be to generate slower code for the reasonable uses of the increment operators, just for the sake of crappy code that abuses them, or else always require extensive flow analysis to prove that you aren't dealing with crappy code, so you can produce the slow (serialized) code only when truly necessary.

Bottom line: if you want to cure this problem, you should be thinking in the opposite direction. Instead of defining what such code does, you should define the language so such expressions simply aren't allowed at all (and live with the fact that most programmers will probably opt for faster compilation over enforcing that requirement).

share|improve this answer
    
IMO, there is little reason to believe that in most cases, the slower instructions are really that much slower than the fast instructions and that these will always have an impact on program performance. I would class this one under premature optimization. –  DeadMG Jun 19 '12 at 12:33
    
Maybe I'm missing something - if nobody's ever supposed to write such code, why care about optimizing it? –  ugoren Jun 19 '12 at 12:55
1  
@ugoren: writing code like a=b[i++]; (for one example) is fine, and optimizing it is a good thing. I don't, however, see the point of hurting reasonable code like that just so something like ++i++ has a defined meaning. –  Jerry Coffin Jun 19 '12 at 13:51
1  
@ugoren The problem is one of diagnosis. The only purpose of not outright disallowing expressions such as ++i++ is precisely that it’s in general hard to distinguish them from valid expressions with side-effects (such as a=b[i++]). It may seem simple enough for us, bu if I remember the Dragon Book correctly then it’s actually an NP-hard problem. That’s why this behaviour is UB, rather than forbidden. –  Konrad Rudolph Jun 19 '12 at 14:20
1  
I don't believe that performance is a valid argument. I struggle to believe that the case is common enough, considering the very slim difference and very fast execution in both cases, for a small performance drop to be noticable- not to mention that on many processors and architectures, defining it is effectively free. –  DeadMG Jun 19 '12 at 17:54

Eric Lippert, a principal designer on the C# compiler team, posted on his blog an article about a number of considerations that go into choosing to make a feature undefined at the language spec level. Obviously C# is a differently language, with different factors going into its language design, but the points he makes are relevant nonetheless.

In particular, he points out the issue of having existing compilers for a language that have existing implementations and also have representatives on a committee. I'm not sure if that's the case here, but tends to be relevant to most C and C++ related spec discussions.

Also of note is, as you said, the performance potential for compiler optimization. While it's true that the performance of CPUs these days are many orders of magnitude greater than they were when C was young, a large amount of C programming done these days are done specifically because of the potential performance gain, and the potential for (hypothetical future) CPU instruction optimizations and multicore processing optimizations would be silly to preclude because of an overly-restrictive set of rules for handling side effects and sequence points.

share|improve this answer
    
From the article you link to, it seems like C# isn't far from what I suggest. Ordering of side effects is defined "when observed from the thread that causes the side effects". I didn't mention multi-threading, but in general C doesn't guarantee much for an observer in another thread. –  ugoren Jun 19 '12 at 10:22

First, let's take a look at the definition of undefined behavior:

3.4.3

1 undefined behavior
behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this International Standard imposes no requirements

2 NOTE Possible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message).

3 EXAMPLE An example of undefined behavior is the behavior on integer overflow

So in other words, "undefined behavior" simply means that the compiler is free to handle the situation any way it wants to, and any such action is considered "correct".

The root of the issue under discussion is the following clause:

6.5 Expressions

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

Emphasis added.

Given an expression like

x = a++ * --b / (c + ++d);

the subexpressions a++, --b, c, and ++d may be evaluated in any order. Furthermore, the side effects of a++, --b, and ++d may be applied at any point before the next sequence point (IOW, even if a++ is evaluated before --b, it's not guaranteed that a will be updated before --b is evaluated). Like others have said, the rationale for this behavior is to give the implementation the freedom to reorder operations in an optimal manner.

Because of this, however, expressions like

x = x++
y = i++ * i++
a[i] = i++
*p++ = -*p    // this one bit me just yesterday

etc., will yield different results for different implementations (or for the same implementation with different optimization settings, or based on the surrounding code, etc.).

The behavior is left undefined so that the compiler is under no obligation to "do the right thing", whatever that may be. The cases above are easy enough to catch, but there a non-trivial number of cases that would be difficult to impossible to catch at compile time.

Obviously, you can design a language such that order of evaluation and the order in which side effects are applied are strictly defined, and both Java and C# do so, largely to avoid the issues that the C and C++ definitions lead to.

So, why hasn't this change been made to C after 3 standard revisions? First of all, there's 40 years' worth of legacy C code out there, and it's not guaranteed that such a change won't break that code. It puts a bit of a burden on compiler writers, as such a change would immediately make all existing compilers non-conforming; everybody'd have to make significant rewrites. And even on fast, modern CPUs, it's still possible to realize real performance gains by tweaking order of evaluation.

share|improve this answer
1  
Very good explanation of the issue. I disagree about breaking legacy applications - the way undefined/unspecified behavior is implemented sometimes changes between compiler version, without any change in the standard. I don't suggest to change any defined behavior. –  ugoren Jun 20 '12 at 6:45

Legacy

The assumption that C could be reinvented today cannot hold. There are so many lines of C codes that have been produced and are daily used, that changing the rules of the game in the middle of the play is just wrong.

Of course you can invent a new language, say C+=, with your rules. But that will not be C.

share|improve this answer
2  
I don't really think we can reinvent C today. This doesn't mean we can't discuss these issues. However, what I suggest isn't really reinventing. Converting undefined behavior to defined or unspecified can be done when updating a standard, and the language would still be C. –  ugoren Jun 19 '12 at 7:48

Declaring that something is defined won't change the existing compilers to respect your definition. That is especially true in the case of an assumption which may have been relied on explicitly or implicitly in a lot of places.

The major issue for the assumption isn't with x = x++; (compilers can easily check for it and should warn), it is with *p1 = (*p2)++ and equivalent (p1[i] = p2[j]++; when p1 and p2 are parameters to a function) where the compiler can't know easily if p1 == p2 (in C99 restrict has been added to spread out the possibility of assuming p1 != p2 between sequence points, so it was deemed that the optimization possibilities were important).

share|improve this answer
    
I don't see how my suggestion changes anything in regard to p1[i]=p2[j]++. If the compiler can assume no aliasing, there's no issue. If it can't, it must go by the book - increment p2[j] first, store p1[i] later. Except for the optimization opportunities lost, which don't seem significant, I see no issue. –  ugoren Jun 19 '12 at 12:02
    
The second paragraph wasn't independent from the first, but an example of the kind of places where the assumption can creep in and will be difficult to track. –  AProgrammer Jun 19 '12 at 13:51
    
The first paragraph states something quite obvious - compilers will have to be changed to comply to a new standard. I don't really think I have a chance to standardize this and make the compiler writers follow. I just think it's worth discussing. –  ugoren Jun 19 '12 at 13:57
    
The problem isn't that one need to change the compilers about any change in the language needs it, it is that the changes are pervasive and at place hard to find. The most practical approach would probably be changing the intermediate format on which the optimizer works, i.e. pretending that x = x++; hasn't been written but t = x; x++; x = t; or x=x; x++; or whatever you want as semantic (but what about diagnostics?). For a new language, just drop out side effects. –  AProgrammer Jun 19 '12 at 14:31
    
I don't know too too much about compiler structure. If I really wanted to change all compilers, I would care more. But maybe treating x++ as a sequence point, as if it was a function call inc_and_return_old(&x) would do the trick. –  ugoren Jun 19 '12 at 14:39

First you have to understand that it's not just x=x++ that is undefined. Nobody cares about x=x++, since no matter what you would define it to there's no point to it. What's undefined is more like "a=b++ where a and b happen to be the same" - i.e.

void f(int *a, int *b) {
    *a = (*b)++;
}
int i;
f(&i, &i);

There several different ways the function might be implemented, depending on what is most efficient for the processor architecture (and for the surrounding statements, in case this is a more complex function than the example). For example, two obvious ones:

load r1 = *b
copy r2 = r1
increment r1
store *b = r1
store *a = r2

or

load r1 = *b
store *a = r1
increment r1
store *b = r1

Note that the first one listed above, the one that uses more instructions and more registers, is the one that you would require to be used in all cases where a and b cannot be proven to be different.

share|improve this answer
    
You indeed show a case where my suggestion results in more machine operations, but it looks insignificant to me. And the compiler still has some freedom - the only real requirement I add is to store b before a. –  ugoren Jun 20 '12 at 6:51

In some cases, this kind of code was defined in the new C++11 Standard.

share|improve this answer
5  
Care to elaborate? –  ugoren Jun 19 '12 at 12:58

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.