Perhaps I'm just naive and/or have forgotten too much C, not that I knew that much, but I'm a bit perplexed as to why this is UB.
It seems like something that should trigger a "we should specify this" reaction when adding these operators, and there is at least one reasonable way to define it which is fairly trivial and easily implementable.
I do remember that this particular code snippet (with a = 5, even) used to be popular as an interview question. I found such questions quite annoying because most interviewers who posed them seemed to believe that whatever output they saw with their compiler version was the correct answer. If you tried explaining that the code has undefined behaviour, the reactions generally ranged from mild disagreement to serious confusion. Most of them neither cared about nor understood 'undefined behaviour' or 'sequence points'.
I remember one particular interviewer who, after I explained that this was undefined behaviour and why, listened patiently to me and then explained to me that the correct answer was 17, because the two post-increments leave the variable as 6, so adding 6 twice to the original 5 gives 17.
I am very glad these types of interview questions have become less prevalent these days. They have, right? Right?
In some sense, and without the interviewer knowing, that is actually a great scenario for an interview.
If you can convince someone in a position of authority that they’re wrong about something technical without upsetting them then you’re probably a good culture fit and someone who can raise the average effectiveness of your team.
IMO, The only reasonable answer if asked this in an interview is “I would not write code where I have to know the answer to this question”
These sorts of things are neat trivia to learn about things like sequence points but 99.9% of the time if it matters in your codebase you're writing something unmaintainable.
> IMO, The only reasonable answer if asked this in an interview is “I would not write code where I have to know the answer to this question”
That's half of a reasonable answer. The other half is "but I do know the answer so if I see it when reviewing or working on someone else's code I can flag it or rewrite it, and explain to them why it is bad".
On one hand I've been using almost the exact statement 25 years ago in my Flash (ecmascript) tutorials to narrow down the point of operator precedence.
I still believe it's a good piece on your powerpoint if you want to teach.
It's easy to fall, easy to grasp, and easy to unroll all the rules - that is, if the rules are actually set in stone.
On the other hand I've been through couple FAANG interviews, and twice I was presented with something similar and after I glanced at it for a half a minute the interviewer quickly proceed to "a ha!, you don't know! the interview is over , but I'm happy to tell you the right answer".
The answers to some questions must be known in order to be able to write a correct program.
In the vast majority of the programming languages, the order of evaluation for the actual parameters passed to a function is undefined. In the few programming languages where the order of evaluation is defined, that is actually a mistake in the design of that programming language.
This is something about which any programmer must be well aware, because when composing function invocations it is very easy to write a function invocation where the result would depend on the order of evaluation of the expressions passed as actual parameters. The arithmetic operators are also function invocations, so that applies to them too.
Heh, one time when I got this style of question[1] (but for JavaScript), I took a glance at it and said "Um ... you really shouldn't write code like that." The interviewer replied, "Oh. Yeah. Fair point." And then went on to another question.
[1] By which I mean predicting the behavior of error-prone code that requires good knowledge of all the quirks of the language to correctly answer.
> I found such questions quite annoying because most interviewers who posed them seemed to believe that whatever output they saw with their compiler version was the correct answer.
Other than the job for most programmers having nothing to do with whether they know the outcome, because hopefully they'd never write something like it or clean it up. And IF they found it they'd hopefully test it - given that it appears to be compiler dependent anyways.
> I am very glad these types of interview questions have become less prevalent these days. They have, right? Right?
Are you referring to the type of interview questions where the question is ill-defined and no one should know the answer, or the type where the question is reasonable and well-defined, but the interviewer doesn't know the answer?
I had a phone screen with Google once where they asked how to determine the length of a stretch of contiguous 1s within an infinite array of 0s. I suggested that, given the starting index i, you can check the index i+2 and then repeatedly square it until you find yourself among the zeroes, after which you can do binary search to find the transition from ones to zeroes.
The interviewer objected that this will grow the candidate end index too quickly, and the correct thing to do is to check index i+1 and then successively double it until you find the zeroes. We moved on.
I passed that phone screen. But I still resent it, because I checked the math later and "successive squaring followed by binary search" and "successive doubling followed by binary search" take exactly the same amount of time.
Genuinely curious, so this is undefined behavior and depends on the compiler. I get that. Java, and other languages, can do these same operations but their compilers produce bytecode that runs on a virtual machine (JVM) compiled to machine code just-in-time. Would this same code in Java possibly yield different results based on the platform the JVM was running on because of the platform specific JIT compiler? Maybe that's part of the origin of the phrase "write once, test everywhere".
Well... tried it on macOS using vanilla gcc, the results surprised me:
$ /bin/cat x.c; gcc -w -o x x.c; ./x
#include <stdio.h>
int main()
{
int a = 5;
a += a++ + a++;
printf("a = %d\n", a);
}
a = 18
Not what I expected.
This must be how it works:
- The first a++ expression results in 5, after a = 6
- The second a++ expression results in 6, after a = 7
- Only then the LHS a is evaluated for the addition-assignment, so we get:
a = 7 + 5 + 6 = 18
Both major compilers yell at you for this nowadays... it's pretty unforgivable IMHO for somebody to be asking it as an exam or interview question if the right answer isn't "undefined":
<source>:5:10: warning: multiple unsequenced modifications to 'a' [-Wunsequenced]
5 | a = a++ + ++a;
|
<source>:5:7: warning: operation on 'a' may be undefined [-Wsequence-point]
5 | a = a++ + ++a;
| ~~^~~~~~~~~~~
What's the reason that C didn't define the order of this?
The horrible undefined behavior of signed integer overflow at least can be explained by the fact that multiple CPU architectures handling those differently existed (though the fact that C even 'attracts' its ill-defined signed integers when you're using unsigned ones by returning a signed int when left shifting an uint16_t by an uint16_t for example is not as forgivable imho)
But this here is something that could be completely defined at the language level, there's nothing CPU dependent here, they could have simply stated in the language specification that e.g. the order of execution of statements is from left to right (and/or other rules like post increment happens after the full statement is finished for example, my point is not whether the rule I type here is complete enough or not but that the language designers could have made it completely defined).
I think the main confusion here comes from the fact that "a" is just a value, not a pointer, where it matters when the value/address which the pointer points at is accessed (before of after the increment of the pointer's own 'value').
Anyway… my C skills are rusty. Maybe I get it wrong. :) In any case I always would use brackets to avoid any ambiguity in constructs like this.
> What's the reason that C didn't define the order of this?
The OP article provides experimental details but annoyingly does not give the big picture w.r.t. C language specifications (provided in the links though).
There are three concepts at interplay here which is at the root of the problem; 1) Expressions (evaluates to a single value) 2) Statements (tells computer to perform an action) and 3) Sequence Points (specific moment during execution when all previous side-effects are guaranteed to be complete).
In C and C++, a sequence point defines any point in a computer program’s execution at which it is guaranteed that all side effects of previous evaluations will have been performed, and no side effects from subsequent evaluations have yet been performed. They are a core concept for determining the validity of and, if valid, the possible results of expressions...
1) An expression's evaluation can be "sequenced before" the evaluation of another expression. (Equivalently, the other expression's evaluation can be "sequenced after" that of the first.)
2) The expression's evaluation is "indeterminately sequenced", meaning that one is "sequenced before" the other, but which is unspecified.
3) The expression's evaluation is "unsequenced", meaning the operations in each expression may be interleaved.
"Order of evaluation of the operands of any C operator, including the order of evaluation of function arguments in a function-call expression, and the order of evaluation of the subexpressions within any expression is unspecified (except where noted below)."
Between consecutive "sequence points" an object's value can be modified only
once by an expression. The C language defines the following sequence points:
Left operand of the logical-AND operator (&&).
Left operand of the logical-OR operator (||).
Left operand of the comma operator.
Function-call operator.
First operand of the conditional operator.
The end of a full initialization expression.
The expression in an expression statement.
The controlling expression in a selection (if or switch) statement.
The controlling expression of a while or do statement.
Each of the three expressions of a for statement.
The expression in a return statement.
Putting all of the above together in OP's code snippet; The "single update rule" fails for the expression since the variable a is modified multiple times between two consecutive sequence points and hence the result is UB.
I am, thankfully, out of this craziness now but it was fun solving ton of such puzzles from Yashavant Kanetkar books while preparing for campus hiring interviews back in 2000. "Test Your C Skills" in particular. Fun times.
"Test Your C Skills" is a published book by Yashavant Kanetkar, apparently published in 2005, and still available in paperback. The document you linked to appears to be a scan of a printed copy of that book, and is almost certainly in violation of copyright. The cover and the title and copyright pages are notably missing.
> The interesting thing here is the Undefined Behavior (UB), well... actually two UBs, thanks to which there are three possible correct answers: 11, 12 and 13.
I'm going top-to-bottom through comments, and there was a similar question, so I'll link my answer here: https://news.ycombinator.com/item?id=48140821 (TL;DR: you are right, but there's another perspective on this)
> The interesting thing here is the Undefined Behavior (UB), well... actually two UBs, thanks to which there are three possible correct answers: 11, 12 and 13.
No, if you invoke undefined behavior any result at all is possible.
So let me start by saying that that blog post was written was 15 years ago and I don't even remember the details of it and what I've written there. But, I have a hot-take on this topic you've touched on!
From a programmer perspective, you are absolutely right. The behaviour is undefined, end of discussion. A programmer should never rely on what they observe as the effective behaviour of an UB. A programmer must avoid creating situations in code that could result in the execution flow venturing into the areas of UB. And - per C and C++ standards - results of UB can be anything (insert the old joke about UB formatting one's disk being a formally correct behaviour).
However, I'm a security researcher, and from the security point of view - especially on the offensive side - we need to know and understand the effective behaviours of UBs. This is because basically all "low-level" vulnerabilities in C/C++ are formally effects of UBs. As such, for the security crowd, it still makes sense to investigate, understand, and discuss the actual observed effects of UBs, especially why a compiler does this, what are the real-world actual variants of generated code (if any) for a given UB for this and other compilers, how can this be abused and exploited, and so on.
My point being - there are two sides to this coin.
With undefined behavior, a conforming compiler can do anything it wants at all, including generating a program that segfaults or something else.
But what often happens in practice is that "Bill's Fly-By-Night-C-Compiler-originally-written-in-the-mid-nineties" implemented it in some specific way (probably by accident) and maintains it as a (probably informal) extension. And almost certainly has users who depend on it, and can't migrate for a myriad of reasons. Anyway, it's hard to sell an upgrade when users can't just drop the new compiler in and go.
At the language level, it is undefined-behavior, and any code that relies on it is buggy at the language level, and non-portable.
Defining it would make those compiler non-conforming, instead of just dependent on defining something that is undefined.
Probably the best way forward is to make this an error, instead of defining it in some way. That way you don't get silent changes in behavior.
Undefined behavior allows that to happen at the language level, but good implementations at least try not to break user code without warning.
Modern compilers with things like UBSan and such makes changing the result of undefined behavior much less of an issue. But most UB is also, "No diagnostic required", so users don't even know they have in their code without the modern tools.
The only point you can conclude out of these discussions, especially in an interview, that it doesn't matter what the answer happens to be on $CC and $ARCH but you wouldn't want anyone to write stuff like that in the first place.
Failing to recognize the dangers would be an instant fail; knowing that something reeks of undefined behaviour, or even potential UB, is enough: you just write out explicitly what you want and skip the mind games.
> If you would like to test your compiler (posting back the results in the comments is really appreciated, especially from strange/uncommon compilers and other languages which support pre- / post- increment ....
Uh, 85% of them show the wrong result so 85% of them clearly do not support pre and post increment.
Except that ++a means increment first and a++ means increment after. And that's well-documented and thoroughly understood. Implying that this is undefined behavior is a cop out and cope. Preposterous and juvenile. If not implemented , it should be. Case closed.
I love such puzzles! I used to use a lot ternary operators in C++ but one day friend of mine told me that I shouldn't nest ternary operators too much because code is too complicated to read - he understands code perfectly, he was just worried about younger programmers. Since then I started to use longer versions of code instead of smart shortcuts - to improve readability of code.
I had to fight through school and university in India with my teachers who believed these were legit questions to ask in written exams. Can't 100% blame them since almost all standard-issue textbooks had them and claimed they'd give predictable output. I thought the same until I noticed the weirdness when running them across different compilers and after I read about UB, sequence points and similar quirks in books that are not total garbage.
Luckily, I ended up with smug smiles in all those cases after showing them the output from different compilers.
I have always hated this crap; the fact that I'm not 100% sure the result of this indicates that maybe the ++ operator (pre or postfix) is something that should be avoided?
I don't do a lot of C anymore, but even when I did, I always would do increments on separate lines, and I would do a +=1, or just a = a + 1. I never noticed a performance degradation, and I also don't think my code was harder to read. In fact I think it was easier since I think the semantics were less ambiguous.
I also started doing this. I feel that "b = expr(a); a++;" expresses what I mean better than "b = expr(a++)": store expr(a) in b, then store a+1 in a. Any good compiler will optimize the same.
After separating a++ onto its own line, replacing a++ with a+=1 or a=a+1 comes down to personal taste in syntax sugar. I vote for a+=1.
The smart nerd will know precisely how to decode that line's results.
The wise nerd will not allow lines like it in their codebase, in the first place and, having seen one, will refactor it (probably involving more lines or parentheses) to make it more clear and easier to maintain.
This is how to keep simpletons out of your code base. Every numeric constant is defined in terms of a different lang quiz. Works well in JS as well of course.
78 comments
[ 0.92 ms ] story [ 78.8 ms ] threadIt‘s the standard technical C++ blog post everybody seems to write.
It seems like something that should trigger a "we should specify this" reaction when adding these operators, and there is at least one reasonable way to define it which is fairly trivial and easily implementable.
I remember one particular interviewer who, after I explained that this was undefined behaviour and why, listened patiently to me and then explained to me that the correct answer was 17, because the two post-increments leave the variable as 6, so adding 6 twice to the original 5 gives 17.
I am very glad these types of interview questions have become less prevalent these days. They have, right? Right?
I just refuse to do interviews like that any more.
If you can convince someone in a position of authority that they’re wrong about something technical without upsetting them then you’re probably a good culture fit and someone who can raise the average effectiveness of your team.
These sorts of things are neat trivia to learn about things like sequence points but 99.9% of the time if it matters in your codebase you're writing something unmaintainable.
That's half of a reasonable answer. The other half is "but I do know the answer so if I see it when reviewing or working on someone else's code I can flag it or rewrite it, and explain to them why it is bad".
I still believe it's a good piece on your powerpoint if you want to teach. It's easy to fall, easy to grasp, and easy to unroll all the rules - that is, if the rules are actually set in stone.
On the other hand I've been through couple FAANG interviews, and twice I was presented with something similar and after I glanced at it for a half a minute the interviewer quickly proceed to "a ha!, you don't know! the interview is over , but I'm happy to tell you the right answer".
That part is not cool.
In the vast majority of the programming languages, the order of evaluation for the actual parameters passed to a function is undefined. In the few programming languages where the order of evaluation is defined, that is actually a mistake in the design of that programming language.
This is something about which any programmer must be well aware, because when composing function invocations it is very easy to write a function invocation where the result would depend on the order of evaluation of the expressions passed as actual parameters. The arithmetic operators are also function invocations, so that applies to them too.
[1] By which I mean predicting the behavior of error-prone code that requires good knowledge of all the quirks of the language to correctly answer.
Other than the job for most programmers having nothing to do with whether they know the outcome, because hopefully they'd never write something like it or clean it up. And IF they found it they'd hopefully test it - given that it appears to be compiler dependent anyways.
Are you referring to the type of interview questions where the question is ill-defined and no one should know the answer, or the type where the question is reasonable and well-defined, but the interviewer doesn't know the answer?
I had a phone screen with Google once where they asked how to determine the length of a stretch of contiguous 1s within an infinite array of 0s. I suggested that, given the starting index i, you can check the index i+2 and then repeatedly square it until you find yourself among the zeroes, after which you can do binary search to find the transition from ones to zeroes.
The interviewer objected that this will grow the candidate end index too quickly, and the correct thing to do is to check index i+1 and then successively double it until you find the zeroes. We moved on.
I passed that phone screen. But I still resent it, because I checked the math later and "successive squaring followed by binary search" and "successive doubling followed by binary search" take exactly the same amount of time.
- The first a++ expression results in 5, after a = 6 - The second a++ expression results in 6, after a = 7 - Only then the LHS a is evaluated for the addition-assignment, so we get: a = 7 + 5 + 6 = 18
The horrible undefined behavior of signed integer overflow at least can be explained by the fact that multiple CPU architectures handling those differently existed (though the fact that C even 'attracts' its ill-defined signed integers when you're using unsigned ones by returning a signed int when left shifting an uint16_t by an uint16_t for example is not as forgivable imho)
But this here is something that could be completely defined at the language level, there's nothing CPU dependent here, they could have simply stated in the language specification that e.g. the order of execution of statements is from left to right (and/or other rules like post increment happens after the full statement is finished for example, my point is not whether the rule I type here is complete enough or not but that the language designers could have made it completely defined).
At least according to this: https://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B#Exp...
I think the main confusion here comes from the fact that "a" is just a value, not a pointer, where it matters when the value/address which the pointer points at is accessed (before of after the increment of the pointer's own 'value').
Anyway… my C skills are rusty. Maybe I get it wrong. :) In any case I always would use brackets to avoid any ambiguity in constructs like this.
The OP article provides experimental details but annoyingly does not give the big picture w.r.t. C language specifications (provided in the links though).
There are three concepts at interplay here which is at the root of the problem; 1) Expressions (evaluates to a single value) 2) Statements (tells computer to perform an action) and 3) Sequence Points (specific moment during execution when all previous side-effects are guaranteed to be complete).
It is the sequence points during the evaluation of expressions which is important to understand here. From https://en.wikipedia.org/wiki/Sequence_point;
In C and C++, a sequence point defines any point in a computer program’s execution at which it is guaranteed that all side effects of previous evaluations will have been performed, and no side effects from subsequent evaluations have yet been performed. They are a core concept for determining the validity of and, if valid, the possible results of expressions...
1) An expression's evaluation can be "sequenced before" the evaluation of another expression. (Equivalently, the other expression's evaluation can be "sequenced after" that of the first.)
2) The expression's evaluation is "indeterminately sequenced", meaning that one is "sequenced before" the other, but which is unspecified.
3) The expression's evaluation is "unsequenced", meaning the operations in each expression may be interleaved.
The "Order of Evaluation" states; (from https://en.cppreference.com/c/language/eval_order)
"Order of evaluation of the operands of any C operator, including the order of evaluation of function arguments in a function-call expression, and the order of evaluation of the subexpressions within any expression is unspecified (except where noted below)."
The "Single Update Rule" states; (from https://www.accellera.org/images/eda/sv-bc/0282.html)
Between consecutive "sequence points" an object's value can be modified only once by an expression. The C language defines the following sequence points:
Putting all of the above together in OP's code snippet; The "single update rule" fails for the expression since the variable a is modified multiple times between two consecutive sequence points and hence the result is UB.For more detailed explanations, see Angelika Langer's Sequence Points and Expression Evaluation in C++ - https://angelikalanger.com/Articles/VSJ/SequencePoints/Seque...
https://www.scribd.com/document/235004757/Test-Your-C-Skills...
There’s UB, so any answer is possible, isn’t it?
I'm going top-to-bottom through comments, and there was a similar question, so I'll link my answer here: https://news.ycombinator.com/item?id=48140821 (TL;DR: you are right, but there's another perspective on this)
No, if you invoke undefined behavior any result at all is possible.
So let me start by saying that that blog post was written was 15 years ago and I don't even remember the details of it and what I've written there. But, I have a hot-take on this topic you've touched on!
From a programmer perspective, you are absolutely right. The behaviour is undefined, end of discussion. A programmer should never rely on what they observe as the effective behaviour of an UB. A programmer must avoid creating situations in code that could result in the execution flow venturing into the areas of UB. And - per C and C++ standards - results of UB can be anything (insert the old joke about UB formatting one's disk being a formally correct behaviour).
However, I'm a security researcher, and from the security point of view - especially on the offensive side - we need to know and understand the effective behaviours of UBs. This is because basically all "low-level" vulnerabilities in C/C++ are formally effects of UBs. As such, for the security crowd, it still makes sense to investigate, understand, and discuss the actual observed effects of UBs, especially why a compiler does this, what are the real-world actual variants of generated code (if any) for a given UB for this and other compilers, how can this be abused and exploited, and so on.
My point being - there are two sides to this coin.
But what often happens in practice is that "Bill's Fly-By-Night-C-Compiler-originally-written-in-the-mid-nineties" implemented it in some specific way (probably by accident) and maintains it as a (probably informal) extension. And almost certainly has users who depend on it, and can't migrate for a myriad of reasons. Anyway, it's hard to sell an upgrade when users can't just drop the new compiler in and go.
At the language level, it is undefined-behavior, and any code that relies on it is buggy at the language level, and non-portable.
Defining it would make those compiler non-conforming, instead of just dependent on defining something that is undefined.
Probably the best way forward is to make this an error, instead of defining it in some way. That way you don't get silent changes in behavior.
Undefined behavior allows that to happen at the language level, but good implementations at least try not to break user code without warning.
Modern compilers with things like UBSan and such makes changing the result of undefined behavior much less of an issue. But most UB is also, "No diagnostic required", so users don't even know they have in their code without the modern tools.
UB = run nethack or Emacs:
https://feross.org/gcc-ownage/
We should have kept this behaviour. It would make UB a lot more unpalatable and easy to find.
Failing to recognize the dangers would be an instant fail; knowing that something reeks of undefined behaviour, or even potential UB, is enough: you just write out explicitly what you want and skip the mind games.
Uh, 85% of them show the wrong result so 85% of them clearly do not support pre and post increment.
Luckily, I ended up with smug smiles in all those cases after showing them the output from different compilers.
int a = 5; a = (++a * a++) + --a; a = ?
I don't do a lot of C anymore, but even when I did, I always would do increments on separate lines, and I would do a +=1, or just a = a + 1. I never noticed a performance degradation, and I also don't think my code was harder to read. In fact I think it was easier since I think the semantics were less ambiguous.
After separating a++ onto its own line, replacing a++ with a+=1 or a=a+1 comes down to personal taste in syntax sugar. I vote for a+=1.
The wise nerd will not allow lines like it in their codebase, in the first place and, having seen one, will refactor it (probably involving more lines or parentheses) to make it more clear and easier to maintain.
The latter approach scales better, in long run.
(this is related to my other comment here https://news.ycombinator.com/item?id=48140821)
This is how to keep simpletons out of your code base. Every numeric constant is defined in terms of a different lang quiz. Works well in JS as well of course.