120

I'm currently learning C++ with the book C++ Primer and one of the exercises in the book is:

Explain what the following expression does: someValue ? ++x, ++y : --x, --y

What do we know? We know that the ternary operator has a higher precedence than the comma operator. With binary operators this was quite easy to understand, but with the ternary operator I am struggling a bit. With binary operators "having higher precedence" means that we can use parentheses around the expression with higher precedence and it will not change the execution.

For the ternary operator I would do:

(someValue ? ++x, ++y : --x, --y)

effectively resulting in the same code which does not help me in understanding how the compiler will group the code.

However, from testing with a C++ compiler I know that the expression compiles and I do not know what a : operator could stand for by itself. So the compiler seems to interpret the ternary operator correctly.

Then I executed the program in two ways:

#include <iostream>

int main()
{
    bool someValue = true;
    int x = 10, y = 10;

    someValue ? ++x, ++y : --x, --y;

    std::cout << x << " " << y << std::endl;
    return 0;
}

Results in:

11 10

While on the other hand with someValue = false it prints:

9 9

Why would the C++ compiler generate code that for the true-branch of the ternary operator only increments x, while for the false-branch of the ternary it decrements both x and y?

I even went as far as putting parentheses around the true-branch like this:

someValue ? (++x, ++y) : --x, --y;

but it still results in 11 10.

Carlos
  • 5,991
  • 6
  • 43
  • 82
aufziehvogel
  • 7,167
  • 5
  • 34
  • 56
  • 5
    "Precedence" is just an emergent phenomenon in C++. It may be simpler to just look at the language grammar directly and see how expressions work. – Kerrek SB Nov 28 '17 at 19:00
  • 26
    We don't care **that** much about the principles. :-) The fact that you have to ask this here indicates the code is never going to pass a code review by your fellow programmers. That makes the knowledge about how this **actually** works less than useful. Unless you want to participate in the [Obfuscated C Contest](http://www.ioccc.org/), of course. – Bo Persson Nov 28 '17 at 19:17
  • [Related in C](https://stackoverflow.com/questions/45348272/error-lvalue-required-as-left-operand-of-assignment) – ad absurdum Nov 29 '17 at 00:51
  • 1
    If you squint a little you can think of the ternary operator as being a binary operator of the form `left ?: right` ; if you think about it that way, does it get easier to see how the precedence rules work when parenthesizing the left and right sides? – Eric Lippert Nov 29 '17 at 06:25
  • 5
    @BoPersson without examples like this to learn from, future reviewers will never learn why they should reject this from production. – Alex Celeste Nov 29 '17 at 10:31
  • 8
    @Leushenko - The warning bells should be ringing anyway. Multiple increments **and** decrements in the same statement (ding, ding, ding!). A ternary operator when you could use if-else (ding, ding, ding!). Wait, are those commas the dreaded comma operator? (ding, DING, DING!) With all those operators, could there be some precedence thing? (ding, ding, ding!) So we are never going to be able to use that. Then why waste time figuring out what it does, if anything? – Bo Persson Nov 29 '17 at 12:09
  • 1
    @BoPersson, it's useful to know what not to do. But it may be enlightening to find out what, exactly, happens if you do that a anyway. – ilkkachu Nov 29 '17 at 13:36
  • 1
    @Bo What's the big deal? There are a lot of questions on SO dealing with largely inconsequential c++ syntax trivia that should never be used in production. It's still useful for understanding terrible code. – Rotem Nov 29 '17 at 15:28
  • 4
    Minor nit: the name for `?` is the *conditional operator*. The term *ternary operator* simply means an operator with three operands. The conditional operator is *one example of a* ternary operator, but a language could (theoretically) have multiple ternary operators. – bta Nov 29 '17 at 16:49
  • 1
    I think this comment thread could do with a bit more context from the book. Does it hint that you are now ready to write code like this? Or is it more like a warning - beware, you may encounter code like this in the wild? – Mr Lister Nov 29 '17 at 18:04
  • 2
    It's not "the ternary operator". It's "the conditional operator" which *happens to be ternary*. – ErikE Nov 29 '17 at 23:12
  • @ErikE Yes, but it's the _only_ ternary operator, so you can say "the ternary operator" and not be ambiguous! – Mr Lister Nov 30 '17 at 07:07
  • @MrLister Some day when there’s another ternary operator, you’ll see why that guy all those years ago (me) was so, so right. And smart. – ErikE Nov 30 '17 at 08:28
  • 1
    @MrLister: You can do it without being ambiguous, but you can't do it without being corrected ;) – Lightness Races in Orbit Nov 30 '17 at 11:26
  • @BoPersson If a code reviewer's warning bells are going off on my code but cannot articulate what the actual problem is with the code or convince me why it is error prone or in bad form, I will chock it up to them being opinionated and I will learn nothing from the exchange. It is essential to understand what makes bad code bad. – Jordan Melo Dec 05 '17 at 00:11
  • 1
    @Jordan - I'd like to quote [Tony Hoare](https://en.wikiquote.org/wiki/C._A._R._Hoare): "There are two ways of constructing a software design: One way is to make it so simple that there are *obviously* no deficiencies, and the other way is to make it so complicated that there are no *obvious* deficiencies." If it is hard to tell what the code does, it is of the second kind. And then I don't really care to work out what the code might do. It's just not useful. – Bo Persson Dec 05 '17 at 00:20
  • 1
    @BoPersson "Simple" is highly subjective. `someValue ? ++x, ++y : --x, --y;` may look very simple to some. Arbitrary rules about what is simple and what is not are fine, but one should be able to articulate reasons why they draw such arbitrary lines. You can espouse as much wisdom about good software design as you like, but wisdom isn't shared by telling others your rules. It's shared by telling others your mistakes. Such is the purpose of questions like this. – Jordan Melo Dec 05 '17 at 01:13
  • 1
    @ErikE I hereby promise to pay you $1,000 when that other ternary operator is invented if it will get you to ease up on the pedantry. If you google "ternary operator" it all points to this conditional operator, because that's what everyone casually calls it. – Barmar Dec 05 '17 at 18:54
  • @Barmar It's not very hard to call it by the right name, pedantry or no. In fact, people don't understand the term "ternary" properly precisely because of this. it's a fundamental in computer science and you're promulgating and propagating ignorance. That's okay, but I'm going to call it out when I see it because I don't like it. Just like you call out pedantry when you see it because you don't like it. – ErikE Dec 05 '17 at 19:03
  • @ErikE I'm an anti-prescriptivist when it comes to language. If enough people call something X, and everyone understands what you mean when you say X, it's a valid name for it. FYI, I've also mostly given up on correcting people when they refer to objects/arrays as JSON. – Barmar Dec 05 '17 at 20:12
  • 1
    @Barmar Great, be an anti-prescriptivist. That is your right. I'm an anti-wrong-nameist. And anti-misconceptionist. Especially when it comes to computer science. And for the record, people don't "lay down" (lie), it isn't "5 or less" (fewer), "John and me think it should be Christine and I" is just flat wrong, "nuclear" is pronounced "NEW-clear" or "NEW-klee-er", and "realtor" is pronounced "REEL-ter". Have fun. – ErikE Dec 05 '17 at 20:16

5 Answers5

125

As @Rakete said in their excellent answer, this is tricky. I'd like to add on to that a little.

The ternary operator must have the form:

logical-or-expression ? expression : assignment-expression

So we have the following mappings:

  • someValue : logical-or-expression
  • ++x, ++y : expression
  • ??? is assignment-expression --x, --y or only --x?

In fact it is only --x because an assignment expression cannot be parsed as two expressions separated by a comma (according to C++'s grammar rules), so --x, --y cannot be treated as an assignment expression.

Which results in the ternary (conditional) expression portion to look like this:

someValue?++x,++y:--x

It may help for readability's sake to consider ++x,++y to be computed as-if parenthesized (++x,++y); anything contained between ? and : will be sequenced after the conditional. (I'll parenthesize them for the rest of the post).

and evaluated in this order:

  1. someValue?
  2. (++x,++y) or --x (depending on boolresult of 1.)

This expression is then treated as the left sub-expression to a comma operator, with the right sub-expression being --y, like so:

(someValue?(++x,++y):--x), --y;

Which means the left side is a discarded-value expression, meaning that it is definitely evaluated, but then we evaluate the right side and return that.

So what happens when someValue is true?

  1. (someValue?(++x,++y):--x) executes and increments x and y to be 11 and 11
  2. The left expression is discarded (though the side effects of increment remain)
  3. We evaluate the right hand side of the comma operator: --y, which then decrements y back to 10

To "fix" the behavior, you can group --x, --y with parentheses to transform it into a primary expression which is a valid entry for an assignment-expression*:

someValue?++x,++y:(--x, --y);

*It's a rather funny long chain that connects an assignment-expression back to a primary expression:

assignment-expression ---(can consist of)--> conditional-expression --> logical-or-expression --> logical-and-expression --> inclusive-or-expression --> exclusive-or-expression --> and-expression --> equality-expression --> relational-expression --> shift-expression --> additive-expression --> multiplicative-expression --> pm-expression --> cast-expression --> unary-expression --> postfix-expression --> primary-expression

AndyG
  • 39,700
  • 8
  • 109
  • 143
  • 10
    Thanks for taking the trouble of unraveling the grammar rules; doing so shows that there is more to C++'s grammar than you will find in most textbooks. – sdenham Nov 29 '17 at 17:36
  • 4
    @sdenham: When people ask why "expression oriented languages" are nice (ie when `{ ... }` can be treated as an expression), I now have an answer => it's to avoid having to introduce a comma operator which behaves in such tricky ways. – Matthieu M. Nov 30 '17 at 07:30
  • Could you give me a link to read about the `assignment-expression` chain? – MiP Dec 19 '17 at 11:13
  • @MiP: I lifted it from the standard itself, you can find it under [gram.expr](http://eel.is/c++draft/gram.expr) – AndyG Dec 19 '17 at 12:15
89

Wow, that's tricky.

The compiler sees your expression as:

(someValue ? (++x, ++y) : --x), --y;

The ternary operator needs a :, it cannot stand by itself in that context, but after it, there is no reason why the comma should belong to the false case.

Now it might make more sense why you get that output. If someValue is true, then ++x, ++y and --y get executed, which doesn't effectively change y but adds one to x.

If someValue is false, then --x and --y are executed, decrementing them both by one.

Pedro A
  • 3,989
  • 3
  • 32
  • 56
Rakete1111
  • 47,013
  • 16
  • 123
  • 162
43

Why would the C++ compiler generate code that for the true-branch of the ternary operator only increments x

You misinterpreted what has happened. The true-branch increments both x and y. However, y is decremented immediately after that, unconditionally.

Here is how this happens: since the conditional operator has higher precedence than comma operator in C++, the compiler parses the expression as follows:

   (someValue ? ++x, ++y : --x), (--y);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^

Note the "orphaned" --y after the comma. This is what leads to decrementing y that has been initially incremented.

I even went as far as putting parentheses around the true-branch like this:

someValue ? (++x, ++y) : --x, --y;

You were on the right path, but you parenthesized a wrong branch: you can fix this by parenthesizing the else-branch, like this:

someValue ? ++x, ++y : (--x, --y);

Demo (prints 11 11)

Community
  • 1
  • 1
Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
5

Your problem is that the ternary expression doesn't really have higher precedence than comma. In fact, C++ can't be described accurately simply by precedence - and it is exactly the interaction between the ternary operator and comma where it breaks down.

a ? b++, c++ : d++

is treated as:

a ? (b++, c++) : d++

(comma behaves as if it has higher precedence). On the other hand,

a ? b++ : c++, d++

is treated as:

(a ? b++ : c++), d++

and the ternary operator is higher precedence.

  • 1
    I think this is still within the realm of precedence since there is only one valid parse for the middle line, right? Still a helpful example though – sudo rm -rf slash Sep 25 '18 at 14:50
3

A point that's been overlooked in answers (though touched on comments) is that the conditional operator is invariably used (intended by design?) in real code as a shortcut for assigning one of two values to a variable.

So, the larger context would be:

whatIreallyWanted = someValue ? ++x, ++y : --x, --y;

Which is absurd on its face, so the crimes are manifold:

  • The language permits ridiculous side effects in an assignment.
  • The compiler didn't warn you that you were doing bizarre things.
  • The book appears to be focusing on 'trick' questions. One can only hope that the answer in the back was "What this expression does is depend on weird edge cases in a contrived example to produce side effects that nobody expects. Never do this."
Taryn
  • 1,670
  • 1
  • 15
  • 22
  • 1
    Assigning one of two variables is the usual case of the ternary operator, but there *are* occasions when it is useful to have an expression form of `if` (for example, the increment expression in a for loop). The larger context might well be `for (x = 0, y=0; x+y < 100; someValue?(++x, ++y) :( --x, --y))` with a loop that can modify `x` and `y` independently. – Martin Bonner supports Monica Dec 06 '17 at 07:52
  • @MartinBonner I'm not convinced, and this example seems to make bo-perrson's point pretty well, as he quoted Tony Hoare. – Taryn Dec 06 '17 at 17:14