Can `throw` be inside a comma subexpression within C++ conditional (ternary) operator?

Viewed 1161

It is well known that throw can be placed as the second or the third operand of C++ ternary operator ?:. But can it be inside a comma subexpression of there operands? It looks like compilers diverge in this regard. Please consider an example:

#include <iostream>

void foo(bool b) {
    int i = b ? 1 : (throw 0); //ok everywhere
    if ( !b )
        (std::cout << "smth\n", throw 0); //ok everywhere
    i = b ? 2 : (std::cout << "smth\n", throw 0); //ok in MSVC only
};

This example is accepted by MSVC, but rejected by both GCC and Clang, demo: https://gcc.godbolt.org/z/6q46j5exP

Though the error message:

error: third operand to the conditional operator is of type 'void', but the second operand is neither a throw-expression nor of type 'void'
    7 |     i = b ? 2 : (std::cout << "smth\n", throw 0);
      |             ^

suggests that it was not rejected intentionally but rather the compiler thinks that the third operand not only has formal type void but actually can return.

According to https://en.cppreference.com/w/cpp/language/operator_other, it seems that GCC/Clang are right since

Either E2 or E3 (but not both) is a (possibly parenthesized) throw-expression.

and here we have parenthesized comma expression just finishing with throw-expression.

According to the standard, is MSVC incorrect in accepting the last line of the example?

2 Answers

Clang and GCC are correct to reject it. It's pretty straightforward:

[expr.cond]

2 If either the second or the third operand has type void, one of the following shall hold:

  • The second or the third operand (but not both) is a (possibly parenthesized) throw-expression ([expr.throw]); 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.
  • Both the second and the third operands have type void; the result is of type void and is a prvalue.

The wording is pretty precise here. It says one operand is a throw-expression when the first bullet applies. And (std::cout << "smth\n", throw 0) is not a throw-expression. It's parenthesized comma expression.

So we can only be in the case of the second bullet, but its conditions don't hold either. So a "shall" requirement is broken, and the program is thus ill-formed.

Now, MSVC may be offering an extension around this, but it's not standard.

As noted by @StoryTeller - Unslander Monica, there is a specification restriction with operands evaluating to void.

I should note, however, that in this case the restriction is trivially bypassed:

#include <iostream>

void foo(bool b) {
    int i = b ? 1 : (throw 0); //ok everywhere
    if ( !b )
        (std::cout << "smth\n", throw 0);

    //  OR

    i = b ? 2 : throw ((std::cout << "smth\n"), 0);

    //  OR

    i = b ? 2 : (std::cout << "smth\n", throw 0, 2);
                                            // ^^^ extra addition
}

The extra , 2 changes the type of (std::cout << "smth\n", throw 0, 2) to int, at which point the quoted rule no longer applies -- the operand is no longer of type void -- and therefore there is no longer any restriction on the operand.

(And now we can question what is the point of the restriction in the standard...)

Related