Search code examples
c++g++language-lawyerconditional-operatorvalue-categories

Value category of conditional operator


Consider the following code:

int x;
int& f() {
  return x ? x : throw 0;
}

With gcc version 7.3.0 (Ubuntu 7.3.0-27ubuntu1~18.04) I get the following compilation error:

cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int’

Note that this compiles just fine in clang. Here is (what I believe to be) the relevant statement from the standard:

N4659 [8.16.2.1] (Conditional Operator):
The second or the third operand (but not both) is a (possibly parenthesized) throw-expression (8.17); the result is of the type and value category of the other.

As far as I understand, x is an lvalue, so it seems to me that clang is right. Am I wrong?


If I had to take a guess, the l-to-rvalue conversion is occurring because the two expressions in the conditional don't have the same type, but because the second is a throw this conversion should be preempted. I'm not familiar with submitting bug reports, but perhaps that would be a better forum for this.
Here are some (probably) more helpful questions about the conditional operator:
Why does this function return an lvalue reference given rvalue arguments?
Error: lvalue required in this simple C code? (Ternary with assignment?)


Solution

  • clang is correct here, the old behavior was to unconditionally convert the value to a prvalue which it looks like gcc still implements.

    This was the subject of DR 1560 which was fixed by the resolution of DR 1550. DR 1560 says:

    A glvalue appearing as one operand of a conditional-expression in which the other operand is a throw-expression is converted to a prvalue, regardless of how the conditional-expression is used:

    If either the second or the third operand has type void, then the lvalue-to-rvalue (7.1 [conv.lval]), array-to-pointer (7.2 [conv.array]), and function-to-pointer (7.3 [conv.func]) standard conversions are performed on the second and third operands, and one of the following shall hold:

    • The second or the third operand (but not both) is a throw-expression (18.1 [except.throw]); the result is of the type of the other and is a prvalue.

    This seems to be gratuitous and surprising.

    and DR 1550 changed the wording in [expr.cond] to what we have now:

    The second or the third operand (but not both) is a (possibly parenthesized) throw-expression; the result is of the type and value category of the other. The conditional-expression is a bit-field if that operand is a bit-field.

    So it looks like gcc is implementing the old behavior while clang is implementing the DR.

    This is the patch that applied DR 1560 to clang. It added the following test:

    namespace DR1560 { // dr1560: 3.5
      void f(bool b, int n) {
        (b ? throw 0 : n) = (b ? n : throw 0) = 0;
      }
      class X { X(const X&); };
      const X &get();
      const X &x = true ? get() : throw 0;
    }
    

    which on godbolt we can see this fails for gcc due to:

    error: lvalue required as left operand of assignment
    4 |     (b ? throw 0 : n) = (b ? n : throw 0) = 0;
      |                    
    

    We have a gcc bug report for a very similar issue which has the following reduced test case:

    I wanted to bump this bug and supply a simpler test-case:

    void blah(int&) {}
    
    int main() {
        int i{};
        blah(true ? i : throw);
    }
    

    result with gcc 6.0:

    prog.cc: In function 'int main()':
    prog.cc:6:15: error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
         blah(true ? i : throw 0);
              ~~~~~^~~~~~~~~~~~~