Earlier quoted context omitted.
> 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. OK. What is the value of a spec to which compliance is impossible?
Welcome to C. But more seriously it's the job of the program to not do undefined things.
Int a = 5; a = a++ + ++a; a =? (2011)
181–190 of 246 posts
Re: Int a = 5; a = a++ + ++a; a =? (2011)
#182Earlier quoted context omitted.
Your compiler does many optimizations that break numerical reproducibility, especially in floats. I reviewed a PR the other day that wrote X=A B+(C D)+E; 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).
Floating point reproducibility and cross platform determinism requires strict adherence to the IEEE standard and disabling of fused instructions.
Re: Int a = 5; a = a++ + ++a; a =? (2011)
#183(When I say "possibility", I meant that it would not be nonconforming, not that I have in mind a specific implementation where such a result can be reproduced.)
A side-effected object may be modified at most once in one evaluation phase.
But this problem already occurs in something simpler like:
b = a++ + a
where the problem is that a modified object is observed by a subexpression in the same evaluation phase, but that subexpression is independent of the side effect.If a is updated in some piecewise, non-atomic way, then it's possible that the right side of the + obtains a half-baked snapshot. Say that a is unsigned and wraps from FF..FF to 00..00, but say this happens byte by byte. The right side of the assignment could access a torn value like FF..00.
Re: Int a = 5; a = a++ + ++a; a =? (2011)
#184Earlier quoted context omitted.
Compilers are not able to prevent you from violating must/shall in the general case. So they're not held to that bar. Unless the standard says not to compile it, it's not a compiler bug. 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 sa…
> 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. OK. What is the value of a spec to which compliance is impossible?
Are you saying, what's the value of a language spec that allows undefined behavior, as C does?
Well, it's that it allows for compiler implementations that aren't too hard to implement and maintain.
It allows for a language that's close enough to hardware (and allows you to do programming on a low level), while still offering a reasonable amount of abstraction to be useful (and usable).
It's also difficult to define a formal system that won't have undefined expressions. Mathematics itself is full of them (in logic, "this sentence is a lie" has no truth value; you can't define the set of all sets, or a set of sets that don't contain themselves; etc).
That said, I think we've settled on a rather silly choice here with the "++" operator.
Personally, I'd do away with the ++ operator in either pre- or post- increment forms, or at least disallow it in arithmetic expressions.
The only thing having it realistically accomplished is saving a few characters when writing a for-loop in C.
Even for that it's not necessary.
The problem with it is that, unlike normal arithmetic operators, it both returns a value and assigns one, which means that you can assign values to several variables in a single arithmetic expression, as in
a = b++;
...which C, in general, allows, as in: a = (b = b + 1);
The result of these two expressions, of course, is different.Now, I have the following religious belief, and it's that arithmetic operators shouldn't have side effects. That's to say, assignment and evaluation should be separate.
So that when I write
x = (arithmetic);
..I could be sure that the only outcome of this computation is changing the value of x.Perhaps calling the function sqrt(x) would summon Cthulhu — I'll read the documentation for it to be sure. But in general, I'd hope that calling abs(x) wouldn't change the value of x to |x| in addition to returning it.
But K&R decided to have fun by saying that "x = 5;" is both an assignment and an expression with a value. Which allows one to write:
x = y = z = 5;
as a parlor trick.That's it, that's the only utility.
Instead of defining this as a special initialization syntax and otherwise disallowing it (as Pyhthon does), they went YOLO and made assignment an expression rather than a mere statement.
Which means that the very useful statement "increase the value of this variable by one" became two expressions with different values.
In an ideal world, the following would be equivalent, and would not evaluate to anything you can assign to a variable:
++x;
x += 1;
x = x + 1;
...while "x++" would not exist at all (or would be equivalent to ++x).And that's how it is in Go. Thompson fixed the design mistake after 4-5 decades of it giving everyone headaches.
Sadly, C++, Java, C# all wanted to be "like C" in basic syntax, so we're stuck with puzzles like this to this day.
TL;DR: if you're asking "what's the value of the spec that makes assignment an expression", i.e. why is making "a = (b = c + d);" valid syntax a good idea, the answer is:
It isn't. It's a bad decision made in 1970s that modern languages like Go no longer support.
Re: Int a = 5; a = a++ + ++a; a =? (2011)
#185What'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…
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:
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.For more detailed explanations, see Angelika Langer's Sequence Points and Expression Evaluation in C++ - https://angelikalanger.com/Articles/VSJ/SequencePoints/Seque...
Re: Int a = 5; a = a++ + ++a; a =? (2011)
#186Earlier quoted context omitted.
Hey! Author here :) 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 p…
> My point being - there are two sides to this coin. No, you're simply wrong. UB means that anything can happen. And from a security perspective, that is vital to understand. The only proper response to this code (or similar UB due to ambiguous sequence points) if found in production is to rewrite it and fire or reeducate the author. Sorry, but some people just aren't competent.
There are two layers two this. On the formal, C and C++ standard lawyering layer, UB can have any result. I of course agree with this as per my previous comment.
However, the compilers are an actual implementation, and actual implementations do things in deterministic ways (even if randomness is involved, realistically it is limited to a certain set of outcomes). As such, in case of UBs it's not "anything can happen" - there is actually a limited set of things that can happen.
And I do believe you've missed the "especially on the offensive side" part of my comment. What you are saying about "if found in production is to rewrite it and fire or reeducate the author" is the defensive security perspective, not the offensive security one. From the offensive security perspective you aren't there to fix the code - you are there to exploit it and hack into the system / leak info / raise your privileges.
Re: Int a = 5; a = a++ + ++a; a =? (2011)
#187Earlier quoted context omitted.
The "shall" in the standard means it's undefined behavior. This is explained in the "Conformance" section, > 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 differen…
> 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. Yes, I see that. I just said they should refuse.
edit: apparently Wunsequenced is enabled by default so clang should warn you out of the box.
Re: Int a = 5; a = a++ + ++a; a =? (2011)
#188Re: Int a = 5; a = a++ + ++a; a =? (2011)
#189Earlier quoted context omitted.
Welcome to C. But more seriously it's the job of the program to not do undefined things.
It's the job of a language designer to define everything.
Re: Int a = 5; a = a++ + ++a; a =? (2011)
#190Earlier quoted context omitted.
> 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".
> 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…
You would lose that wager.
What I mean by "I do know the answer" is that I know that this is undefined behavior and why it is undefined behavior and that different compilers can give different results and also that even if I test the compiler I use to see what it does I can't count on that not changing any time the compiler gets updated.