void.pointer
void.pointer

Reputation: 26365

Confusion with commas in ternary expression

I found the following interesting code today:

SomeFunction(some_bool_variable ? 12.f, 50.f : 50.f, 12.f)

I created a small sample to reproduce the behavior:

class Vector3f
{
public:
    Vector3f(float val)
    {
        std::cout << "vector constructor: " << val << '\n';
    }
};

void SetSize(Vector3f v)
{
    std::cout << "SetSize single param\n";
}

void SetSize(float w, float h, float d=0)
{
    std::cout << "SetSize multi param: " << w << ", " << h << ", " << d << '\n';
}

int main()
{
    SetSize(true ? 12.f, 50.f : 50.f, 12.f);
    SetSize(false ? 12.f, 50.f : 50.f, 12.f);
}

(Live Sample)

The result I get from running the above code is:

clang++ -std=c++14 -O2 -Wall -pedantic -lboost_system -lboost_filesystem -pthread main.cpp && ./a.out
main.cpp:29:20: warning: expression result unused [-Wunused-value]
    SetSize(true ? 12.f, 50.f : 50.f, 12.f);
                   ^~~~
main.cpp:30:21: warning: expression result unused [-Wunused-value]
    SetSize(false ? 12.f, 50.f : 50.f, 12.f);
                    ^~~~
2 warnings generated.
SetSize multi param: 50, 12, 0
SetSize multi param: 50, 12, 0

What I was expecting in both cases was that a single parameter would be passed to SetSize(float). However, two parameters are passed which I find extremely confusing (especially since ternary has precedence over comma; so I assumed the comma was not delimiting function arguments in this case). For example, if using true, the ternary should result in 12.f, 50.f. In this expression, the value to the left of the comma gets dropped/ignored, so I'd expect the end result to be:

SetSize(50.f);

The second part of the confusion is that whether we use true or false in the ternary, the same 2 values are passed to the function. The true case should be h=12, w=50 I'd think...

I see the compiler is trying to warn me about something, but I can't quite understand what's going on. Can someone decompose this logic and explain the result in a step by step fashion?

Upvotes: 34

Views: 2058

Answers (3)

dbush
dbush

Reputation: 224232

While the second part of the ternary operator is self contained, the third part is not. The grammar is as follows:

conditional-expression:

logical-or-expression

logical-or-expression ? expression : assignment-expression

So your function call is effectively this:

SetSize((true ? (12.f, 50.f): 50.f), 12.f)

So the ternary expression true ? (12.f, 50.f): 50.f gets evaluated as the first parameter to the function. Then 12.f is passed as the second value. The comma in this case is not the comma operator but the function parameter separator.

From section 5.18 of the C++ standard:

2 In contexts where comma is given a special meaning, [ Example: in lists of arguments to functions (5.2.2) and lists of initializers (8.5) — end example ] the comma operator as described in Clause 5 can appear only in parentheses. [ Example:

f(a, (t=3, t+2), c);

has three arguments, the second of which has the value 5 . — end example ]

If you want the last two subexpressions to be grouped together, you need to add parenthesis:

SetSize(true ? 12.f, 50.f : (50.f, 12.f));
SetSize(false ? 12.f, 50.f : (50.f, 12.f));

Now you have a comma operator and the single argument version of SetSize gets called.

Upvotes: 30

Sergey Kalinichenko
Sergey Kalinichenko

Reputation: 726779

This is because C++ does not treat the second comma as a comma operator:

The comma in various comma-separated lists, such as function argument lists f(a, b, c) and initializer lists int a[] = {1,2,3}, is not the comma operator.

As far as the first comma is concerned, C++ has no other option but to treat it as a comma operator. Otherwise, the parse would be invalid.

One simple way of viewing it is to think that as soon as C++ parser finds ? in context where comma separators are allowed, it looks for the matching : to complete the first part of the expression, and then matches as little as is necessary to complete the second expression. The second comma would not be treated as an operator even if you remove the two-argument overload.

Upvotes: 17

Lightness Races in Orbit
Lightness Races in Orbit

Reputation: 385224

The compiler is warning you that you're throwing away precisely 50% of your floating-point literals.

Let's decompose it.

// void SetSize(float w, float h, float d=0)
SetSize(true ? 12.f, 50.f : 50.f, 12.f);
//      ^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^

Here we present an expression using the conditional operator as the first argument, and the literal 12.f as the second argument; the third argument is left at its default (0).

Yes, really.

It's parsed like this (because there's no other valid way to parse it):

SetSize( (true ? 12.f, 50.f : 50.f), 12.f);
//        ^^^^^^^^^^^^^^^^^^^^^^^^   ^^^^

The value of the second argument is straightforward, so let's examine the first:

true ? 12.f, 50.f : 50.f

This means:

  • If true, the result is 12.f, 50.f
  • Otherwise, the result is 50.f

Well, true is always true, so we can immediately discount the second option.

And the expression 12.f, 50.f utilises the comma operator, which evaluates both operands then chucks away the first and results in the second, i.e. 50.f.

Therefore, the whole thing is actually:

SetSize(50.f, 12.f);

If this is not some arcane and pointless programming "puzzle", it's a remarkably stupid piece of code, with an uneducated programmer hoping to "unpack" the expression into something more equivalent to:

SetSize(
   (true ? 12.f : 50.f),
   (true ? 50.f : 12.f)
);

… which is still terrible and useless code, because true is still always true.

(Obviously, the values are different in the case that false is written instead, but the same logic applies.)


The true case should be h=12, w=50 I'd think...

It is. That's what the output you posted says. It's clearer when you don't arbitrarily re-arrange the arguments, i.e. they're w=50 h=12.

Upvotes: 9

Related Questions