int a = 5;
a += a++ + a++;
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?
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".
> The other half is "but I do know the answer
Except you don't!If you claim to know the answer you've made a grave mistake and fooled yourself.
If you ran the code in a compiler and used that to conclude "this is the answer" rather than "this is an answer" then now is a great time to learn how easy it is to fool yourself. You just need you ask yourself what assumptions you made. I'll wager you assumed all compilers process this line in the same way.
Or just RTFA, or Susam's, as that's exactly what they are about. They explain why this is undefined behavior.
| The first principle is that you must not fool yourself — and you are the easiest person to fool.
- FeynmanThat's the feedback I would want, and it's the feedback I give to my colleagues in reviews. Actually I tend to be too verbose, so you might get a full paragraph explaining what the ISO document says and that you shouldn't assume it does whatever it is your compiler says.
My actual feelings for this specific case are that the language is defective, but if we're wedded to a defective language then the reviews need to call out such usage.
> Actually I tend to be too verbose, so you might get a full paragraph explaining what the ISO document
I'm verbose too, but I love it when others are. Honestly, it's usually easy to triage (and I write to try to make it easy). I like verbosity because learning why means I not only won't make that mistake again but I won't make any similar mistakes again.Verbosity isn't bad. Not everything needs to be a fucking tweet
The behavior of "int a = 5; a = a++ + ++a;" is undefined. There is no guarantee of a numeric result, because there is no guarantee of anything.
I think the objection thaumasiotes has raised there is valid and I have made an attempt to answer it as well in the same thread.
A conforming implementation could reject it at compile time, or generate code that traps, or generate code that set a to 137, or, in principle, generate code that reformats your hard drive. Some of these behaviors are unlikely, but none are forbidden by the language standard.
Susam's post doesn't make this clear. The quotes from K&R say that the modifications to the variable may take place in any order, but they don't directly say that doing this is Undefined Behavior, which would make it permissible to do anything, including e.g. interpreting the increments as decrements.
The C99 standard is quoted saying this:
>> Between the previous and next sequence point an object shall have its stored value modified at most once by the evaluation of an expression.
It's possible that something else in the standard defines noncompliance with this clause as Undefined Behavior. But that's not the most intuitive interpretation; what this seems to say, to me, is that the line of code `a = a++ + ++a` should fail to compile, because it's not in compliance with a requirement of the language. Compilers that produce any result at all are suffering from a bug.
(It seems more likely that the actual intent is to specify that, given the line of code `b = a++ + ++a`, with a initially equal to 5, the compiler is required to ensure that the value stored at the address of a is never equal to 6 - that it begins at 5, and at some indefinite point it becomes 7, but that there is no intermediate stage between them. But I find the 'compiler failure on attempt to put multiple modifications between two sequence points' interpretation preferable.)
> 2. If a ‘‘shall’’ or ‘‘shall not’’requirement that appears outside of a constraint is violated, the behavior is undefined. Undefined behavior is otherwise indicated in this International Standard by the words ‘‘undefined behavior’’ or by the omission of any explicit definition of behavior. There is no difference in emphasis among these three; they all describe ‘‘behavior that is undefined’’.
Compilers will not refuse to compile the code, indeed the blog post we are all commenting on reports the results from a bunch of different compilers. Historically the reason the C standard specified a lot of undefined behavior is that the actually existing C compilers at the time compiled the code but disagreed about the output.
Yes, I see that. I just said they should refuse.
Also, imagine a situation where the line of code actually lists three different variables, but all three of them are passed in by address. It quickly becomes impossible for the compiler to know you violated the spec by reusing the same variable. And even optimizations that make sense here could corrupt the value pretty badly and possibly lead to worse errors.
OK. What is the value of a spec to which compliance is impossible?
It lets you tell people you have a spec? It makes it easy for compiler developers to dismiss bug reports with "your code violated the spec"?
But more seriously it's the job of the program to not do undefined things.
> The precedence and associativity of operators is fully specified, but the order of evaluation of expressions is, with certain exceptions, undefined, even if the subexpressions involve side effects. That is, unless the definition of the operator guarantees that its operands are evaluated in a particular order, the implementation is free to evaluate operands in any order, or even to interleave their evaluation. However, each operator combines the values produced by its operands in a way compatible with the parsing of the expression in which it appears. This rule revokes the previous freedom to reorder expressions with operators that are mathematically commutative and associative, but can fail to be computationally associative. The change affects only floating-point computations near the limits of their accuracy, and situations where overflow is possible.
So I think, the text in K&R serves as warning against writing such code, at best. The C99 draft has more relevant language. From § 4. Conformance:
> If a "shall" or "shall not" requirement that appears outside of a constraint is violated, the behavior is undefined. Undefined behavior is otherwise indicated in this International Standard by the words "undefined behavior" or by the omission of any explicit definition of behavior. There is no difference in emphasis among these three; they all describe "behavior that is undefined".
This along with the § 6.5 excerpt already mentioned in my post implies a += a++ + a++ to be undefined. When I get some more time later, I'll make an update to my post to include the § 4. Conformance language too for completeness.
Thank you for the nice comment!
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.
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.
"What does this produce?" and expecting an answer of "17" is a bad question even if UB didn't mean the expected answer is wrong.
Having an understanding of how the code gets transformed into machine code helps. For this case, there's the basic idea that `a++` will boil down to three basic conceptual operations: fetch, add, and store, and those can be potentially interleaved with other parts of the statement. In something like `a++ + ++b` the interleaving doesn't affect the outcome no matter how it's done. In `a++ + ++b` the interleaving can affect the outcome, and that's your sign that something might be wrong.
Any memory safety issue in C code had to involve UB at some point. And you can see how prevalent those are, and deduce how not-particularly-great we are at keeping track of UB.
I'm not sure about that. Knowing assembly is not a substitute for knowing how the language is defined. Sometimes C/C++ programmers with some assembly knowledge reason themselves into thinking that what they're asking of the language must have well-defined behaviour, when in fact it's undefined behaviour. It doesn't really matter whether interleaving order can change the output. (++i)++ is, apparently [0], undefined behaviour in C but has well defined behaviour in C++.
I write "in general" because, as with other forms of memory reinterpretation (memcpy or copy through a character type), evaluating a trap representation triggers UB.
I would argue that most languages only have one compiler so it doesn't matter what is in the specification.
No, and it's also well defined in languages like C#.
If we're talking about this specific example at least. No sequence point issues like that in Java.
The ++x is a "pre-increment", meaning the value of the variable is incremented prior to evaluating the expression, while the "post-increment" "x++" is the other way around: the expression evaluates to x, then x is incremented afterwards.
All expressions are left-to-right.
The reason the question is tricky is because those operators change the value of a as the full expression is progressively executed.
It's not immediately clear to me what the answer in Java would be.
Just take a++ + ++a for example:
If the value if `a` is hoisted by the jvm then it could be 5++ + ++5, so 5 + 6.
But if it's executed left to right and `a` is looked up every time, then it becomes 5++ + ++6, so 5 + 7.
but still, if it were, it was and remained, as gp points out, bad practice...
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.
<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;
| ~~^~~~~~~~~~~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.
Your phone screen story is quite nice. When I read your question, I would have answered with successive doubling as well. In fact, I faced the same question at an AWS interview a long time ago. The question was mathematically the same question but formulated differently. I answered with the doubling solution too, which leads to an O(log n) time solution, asymptotically. Your interviewer's immediate objection to your squaring solution seems like a major failure in their intuition. When I read your solution, purely by intuition, that is, without resorting to any rigorous reasoning, I felt: wow, that's interesting, your solution would land on the zero region in merely O(log log n) time. Why didn't I think of it? I think your solution should spark interest rather than dismissal in a curious person. Of course, the binary search after that to find the exact transition point blows up the time consumed back to O(log n).
Once again, thanks for these really interesting comments!
[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 just refuse to do interviews like that any more.
$ /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
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).
This isn't quite the same case, but it's a good illustration of the effect: on gcc, if you have an expression f(a(), b()), the order that a and b get evaluated is [1] dependent on the architecture and calling-convention of f. If the calling convention wants you to push arguments from right to left, then b is evaluated first; otherwise, a is evaluated first. If you evaluate arguments in the right order, then after calling the function, you can immediately push the argument on the stack; in the wrong order, the result is now a live variable that needs to be carried over another function call, which is a couple more instructions. I don't have a specific example for increment/decrement instructions, but considering extremely register-poor machines and hardware instruction support for increment/decrement addressing modes, it's not hard to imagine that there are similar cases where forcing the compiler to insert the increment at the 'wrong' point is similarly expensive.
Now, with modern compilers using cross-architecture IRs as their main avenue of optimization, the benefit from this kind of flexibility is very limited, especially since the penalties on modern architectures for the 'wrong' order of things can be reduced to nothing with a bit more cleverness. But compiler developers tend to be loath to change observable behavior, and the standards committee unwilling to mandate that compiler developers have to modify their code, so the fact that some compilers have chosen to implement it in different manners means it's going to remain that way essentially forever. If you were making a new language from scratch, you could easily mandate a particular order of evaluation, and I imagine that every new language in the past several decades has in fact done that.
[1] Or at least was 20 years ago, when I was asked to look into this. GCC may have changed since then.
With modern register allocators and larger register sets, code generation impact from following source evaluation is of course lower than it used to be. Some CPUs can even involve stack slots in register renaming: https://www.agner.org/forum/viewtopic.php?t=41
On the other hand, even modern Scheme leaves evaluation order undefined. It's not just a C issue.
Anyway, yes, this one example has an obvious order it should be applied. But still, something like it shouldn't be allowed.
That would be nice, but don't forget the more general case of pointers and aliasing:
int a = 5;
int *pa = &a;
printf("%d", (a++ + ++*pa));
The compiler cannot statically catch every possible instance of a statement where a variable is updated more than once.Look at the addressing modes for the PDP-11 in https://en.wikipedia.org/wiki/PDP-11_architecture and you'll see you can write (R0)+ to read the contents of the location pointed to by R0, and then increment R0 afterwards (so a post increment).
Back in the day, compilers were simple and optimisations weren't that common, so folding two statements into one and working out that there were no dependencies would have been tough with single pass compilers.
You could argue that without such instructions, C wouldn't have been embraced quite so enthusiastically for systems programming, and the world would have looked rather different.
C just wouldn't be C without things like a[i++]
I think if anything people have been leaning more and more into expressions over statements, because when everything is an expression you end up being able to walk the gradient of complexity a bit more nicely than when you end up with a thing that just has to be broken down to a bunch of statements.
Both are favorite idioms of C developers. And they are ok if done correctly, clearer than the alternative. They are also unnecessary in modern languages, so those shouldn't copy it (yeah, Python specifically).
The reason that these operators pull their weight in C is because iteration over arrays is achieved by manual incrementation (usually via the leading clauses of the for-loop) followed by direct indexing. Languages with a first-class notion of iteration don't directly index in this way, which overwhelmingly eliminates not only the vast majority of array indexing operations in codebases but also the need to manually futz with the inductive loop variable. Case in point, Rust doesn't have `++` in any form, and it doesn't miss it, because Rust has first-class iteration; on the then relatively rare occasion where you want do want to increment, you can do `+=1`, which doesn't have the footguns of `++` due to assignment being a statement rather than expression, while leading to a simpler language due to leveraging the existing `+=` syntax rather than needing a whole new set of operators.
> which doesn't have the footguns of `++` due to assignment being a statement rather than expression,
So then I implement the local equivalent of inc( v ) and ... same issue, right? Plus with rust macros is there any technical reason you can't trivially implement ++ for yourself? That's the case for most lisps that I touched on earlier.
I'd say that when you're writing a mildly complex loop that involves pointer juggling, one should prefer to be defensive and explicit rather than cleverly trying to compress everything into one-liners.
> So then I implement the local equivalent of inc( v ) and ... same issue, right?
This isn't done in Rust because there's no benefit. It's rare to find an occasion where it's necessary to do something tricky enough to forego using iterators, and when working with raw pointers Rust code just plain doesn't use basic addition for pointer arithmetic; instead it has a variety of pointer arithmetic methods for being explicit about the desired semantics (e.g. ptr::add, ptr::offset, ptr::wrapping_add, etc).
> Plus with rust macros is there any technical reason you can't trivially implement ++ for yourself?
There's not, but people might look at you sideways. Here, I implemented it for you: https://play.rust-lang.org/?version=stable&mode=debug&editio... . It expands to nested blocks with internal assignments, which results in a well-defined semantics following the defined order of evaluation in Rust.
People tend to use FP abstractions for the "x[i++] = f(y[j++])" though, not iteration.
Just switching between left to right or right to left wouldn't be that useful but it also permits to interleave the subexpression evaluation. Grouping memory fetches/writes, taking into account how many execution units and registers of different kinds a CPU has can have some performance benefits.
For example if you have something like `++a[0] + ++a[1] + ++a[2] + ++a[3]` instead of evaluating each increment one by one both GCC and Clang will vectorize it loading all 4 values from memory using single simd instruction, incrementing and then writing result back to memory. And if you add fifth one (but not 8) which needs to be handled using regular instruction, that will be done after the first 4. If standard defined that left subexpression of addition is fully evaluated before the right expression that wouldn't be allowed.
And when I checked 3 different compilers, each of them chose a different way to use FMAs.
Even with integer math, you can get different numerical results via UB (e.g. expressions with signed overflow one way and not another).
I didn't open TFA but my first thought was "Is this even defined?".
It kinda make sense that suck fucktardedness could be not 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.
Actually the compiler (at least clang) warns about this:
$ gcc -W -Wall test.c -o test
test.c:8:7: warning: multiple unsequenced modifications to 'a' [-Wunsequenced]
a = a++ + ++a;
^ ~~
1 warning generated.
The undefined behaviour stems from the fact that "a" is modified multiple times between the "sequence points" (so it's irrelevant to the actual problem that this happens with ++, --, pre-, or, post-, or in which order) We only can modify the variable safely once on the right side without entering bizarro world.A construct like this certainly can be confusing.
Luckily, I ended up with smug smiles in all those cases after showing them the output from different compilers.
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.
As a programmer, the solution to "int a = 5; a = a++ + ++a;" is to decide what you result you wanted, and write code that will produce that result, and probably to pass options to the compiler that tell it to detect this kind of problem and print a warning. (On my system, the result happens to be 12; if that's what I want, I'll write "int a = 12;").
But if you have an existing program that includes that code, it can be useful to look into the actual behavior (for all the compilers that might be used to compile the code, with all possible options, on all possible target systems). Fixing the code should be part of that process, but you might still have running systems with the old bad code, and you need to understand the risks.
But producing some numeric result is not the only possible behavior, even in real life. Compilers can assume that the code being compiled does not have undefined behavior, and generate code based on that assumption. The results can be surprising.
As for formatting your disk, that's not just a theoretical risk. If a program has enough privileges that it can format your disk deliberately, it's possible that it could do so accidentally due to undefined behavior (for example, if a function pointer is corrupted).
The problem is that it’s not specified which should be picked, but all pick something.
The obvious counterpoint in this particular instance is that there's no good reason not to make such an awful expression a compile time error.
I also personally think that evaluation order should be strictly defined. I'm unclear if the current arrangement ever offers noticable benefits but it is abundantly clear that it makes the language more difficult to reason about.
Today we don't have nearly the variety of architectures, so they in theory C doesn't need nearly as much UB (like more modern languages).
Although there is one modern case where C's "anything goes" attitude has actually helped: CHERI works pretty well with C/C++ even though pointers are double the size they normally are, because doing so many things with pointers is UB (I assume because of segmented memory). CHERI is a slightly awkward target for Rust because Rust makes more assumptions about pointers - specifically that pointers and addresses are the same size.
The reality is these are all edge conditions rarely encountered.
This doesn't really help portability all that much.
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.
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.
LL and LR parser generates different derivation, and as such it is deterministically non-deterministic, hence UB.
awk 'BEGIN{a=5; a = a++ + ++a; print a}'
12"When side effects happen is implementation-defined. In other words, it is up to the particular version of awk."
:/ $ awk 'BEGIN{a=5; a = a++ + ++a; print a}'
12
:/ $ which awk
/system/bin/awk
:/ $ awk --version
awk version 20240728I 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.
I wouldn't be surprised if someone read `b = expr(a++)` to indicate that `a` is incremented, and then passed into `expr`, especially considering that it is within parentheses. The fact that it does it after passing it in is weird, and not obvious, at least not in my opinion. In my mind, there's no reason not to do what you suggested, or do the increment of `a` on the line before if you want the prefix.
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)
I <- I++
On the next hour another professor was giving lecture on C++ programming. I asked him the question: what would happen if we compiled i = i++
He went into some deep elaboration on it, but reassumed that only idiot would write like this...If you write:
int i = 0;
i = i++;
and never use the value of i, the declaration and assignment are likely to be optimized out. (The behavior of the assignment is undefined, so this is a valid choice).If you print the value of i, the compiler can still optimize away the computation, but is perhaps less likely to do so.
The solution, of course, is not to write code like that. Decide what you want to do, and write code that does that. "i = i++" will never be the answer to "how do I do this?", and wouldn't be even if the behavior were well defined. If you want i to be 1, write "int i = 1;".
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.
You should start here:
https://c-faq.com/expr/evalorder2.html
I cannot recommend the C FAQ enough. It is written in an accessible way and contains proper references to textbooks and standards.
Disclosure: I was one of the contributors.
https://www.scribd.com/document/235004757/Test-Your-C-Skills...
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.
const DEFAULT_SELECTION = true + true
const BASE_PRICE = 4 * parseInt(0.0000001)
const BILLING_DAY_OF_MONTH = a++ + ++aThe main, I would say, defining, feature of Java is "no of undefined behavior". Aka "write once, run everywhere".
It‘s the standard technical C++ blog post everybody seems to write.
int a = 5;
int b = a++;
if it gives b==5 in this circumstance (which I would say is the correct value), then it seems that giving 13 for a++ + ++a is a bug in the compiler. I kind of feel like giving 6 as an answer would also be a bug in the compiler since postfix-++ should return the old value and then increment. int a = 5;
int b = a++;
has well defined behavior. The first line initializes a to 5. The second initializes b to 5 and sets a to 6. (The language doesn't specify the order of the two operations of assigning a value to be and incrementing a, but in this case it doesn't matter.)Giving 13 for a++ + ++a is not a bug in the compiler. It's a bug in the code.
The correct answer to "what does a++ + ++a do" is "it gets rejected in code review and replaced with code that expresses the actual intent.
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)
#include <stdio.h>
int main() {
int a = 5;
a = a++ + ++a;
printf("%d\n", a);
return 0;
}
x64 msvc v19.50 VS18.2 output: example.c
ASM generation compiler returned: 0
example.c
Execution build compiler returned: 0
Program returned: 0
13
x86-64 gcc 16.1 output: ASM generation compiler returned: 0
Execution build compiler returned: 0
Program returned: 0
12
armv8-a clang 22.1.0 output: <source>:5:10: warning: multiple unsequenced modifications to 'a' [-Wunsequenced]
5 | a = a++ + ++a;
| ^ ~~
1 warning generated.
ASM generation compiler returned: 0
<source>:5:10: warning: multiple unsequenced modifications to 'a' [-Wunsequenced]
5 | a = a++ + ++a;
| ^ ~~
1 warning generated.
Execution build compiler returned: 0
Program returned: 0
12Uh, 85% of them show the wrong result so 85% of them clearly do not support pre and post increment.
int a = 5; a = (++a * a++) + --a; a = ?