Conditional expression produces different type between MSVC and GCC/Clang
Asked Answered
S

1

8

This code works in GCC and Clang, bug not on MSVC:

#include <concepts>
#include <utility>

struct S {};

const S&& f();
S g();

static_assert(std::same_as<decltype(false ? f() : g()), const S>);

https://godbolt.org/z/99rMPzecM

MSVC thinks decltype(false ? f() : g()) is const S&&

Which one is right? And why?

Sinistrodextral answered 19/4 at 14:13 Comment(1)
If f function returns not-const rvalue reference S&& f(), then all three compilers agree that the result of ternary operator has not-reference type S. So GCC/Clang are at least more consistent in your case, and MSVC changes its mind unexpectedly.Moynahan
C
9

Based on my understanding, all compilers are wrong, and the program should be ill-formed.

Related work

There is a submitted but not yet numbered CWG issue where @BrianBi explains this scenario, and also concludes that the wording implies ill-formedness.

On the other hand, @Barry has compiled these combinations of types in the conditional operator in P3177R0, and claims that const S would be produced. However, this data cannot be used to conclude anything because it was collected by simply taking GCC output, not by reviewing standard wording.

Language Lawyering

To determine the correct type in the end, consider the rules for determining the type of the conditional operator in [expr.cond]. Among other things, in ... ? f() : g(), the compiler attempts to convert f() to g() and g() to f(), as described in [expr.cond] p4.

Step 1: Determining target types

For the purpose of the aforementioned conversions, the compiler determines target types for both implicit conversion sequences.

Note that:

  • f() is of type const S&&, which would turn into an xvalue of type const S prior to any analysis ([expr.type] p1), and
  • g() is a prvalue of type S.

Conversion from f() to a type related to S

This conversion takes place according to [expr.cond] p4.3:

If E2 is a prvalue or if neither of the conversion sequences above can be formed and at least one of the operands has (possibly cv-qualified) class type: [...]

g() is a prvalue, so this is the relevant case.

Since const S and S are the same class type (ignoring cv-qualification), but S is less cv-qualified, [expr.cond] p4.3.1 does not apply, but rather, lvalue-to-rvalue conversion is applied to E2, which is g() ([expr.cond] p4.3.3):

otherwise, the target type is the type that E2 would have after applying the lvalue-to-rvalue, array-to-pointer, and function-to-pointer standard conversions.

Result: Target type is S


Note: E2 is g() and none of the listed conversions are applicable, however, that should be fine. Since you obviously can't apply all three listed conversions, the intent of the wording is that any of these conversion are applied, if possible. In other words, the target type is simply E2 with nothing applied.

Conversion from g() to a type related to const S

If E2 is an xvalue, the target type is “rvalue reference to T2”, but an implicit conversion sequence can only be formed if the reference would bind directly.

- [expr.cond] p4.2

All of the conditions here are satisfied, given that f() is an xvalue, and a reference const S&& can bind directly to a prvalue of type S ([dcl.init.ref] p5.3.1).

Result: Target type is "rvalue reference to const S"

Step 2: Implicit conversions

Now, the compiler checks if implicit conversions can be performed with the given target types.

  • f() can be converted to S using the implicitly-defined (trivial) copy constructor which would be called as part of a user-defined conversion sequence, and
  • g() can be converted to const S&& by simply binding a reference.

We should now run into [expr.cond] p4, sentence 5:

If both sequences can be formed, or one can be formed but it is the ambiguous conversion sequence, the program is ill-formed.

The compiler should reject the code at this point, however, none do.

The GCC/clang perspective

If we kept going as if the program wasn't well-formed, we would run into [expr.cond] p6:

Otherwise, the result is a prvalue. [...] Otherwise, the conversions thus determined are applied, and the converted operands are used in place of the original operands for the remainder of this subclause.

The result is a prvalue and we apply one final round of lvalue-to-rvalue conversion to make g() (since it has been converted to an rvalue reference). Apparently, GCC thinks that this prvalue should be of type const S, but we should have never gotten to this point.

Since the result is a prvalue and decltype is not being applied to an unparanthesized id-expression, decltype should produce S or const S ([dcl.type.decltype] p1.6).

The MSVC perspective

MSVC probably thinks that one of the two conversions failed, in which case the result can be const S&&:

If the second and third operands are glvalues of the same value category and have the same type, the result is of that type and value category and it is a bit-field if the second or the third operand is a bit-field, or if both are bit-fields.

- [expr.cond] p5

However, it's unclear why MSVC thinks that f() cannot be converted to S, which would have resulted in a scenario where both operands are xvalues of type const S&& now.

Cinquefoil answered 19/4 at 15:5 Comment(16)
Well, by all means, I must have gotten something wrong; I'm wondering why no compiler rejects this. It's does look to me like the reference binds directly, so both conversion sequences can be formed, and that should be a no-go. But when no compiler rejects it and it should be, chances are, we're not reading this correctly.Cinquefoil
@JanSchultke the conversion f() to g() can not be formed. Because S is not at least as cv-qualified as const SSinistrodextral
@Sinistrodextral Which means it goes to the next applicable case, wg21.link/expr.cond#4.3.3 so the target type is SLeukocyte
@Leukocyte I've updated the question and you're close, but lvalue-to-rvalue conversion for f() will not drop the cv-qualifier, so the resulting type is const S, not S.Cinquefoil
@Leukocyte g() is a prvalue, can lvalue-to-rvalue be applied to prvalue? After conversion, const S is still different from SSinistrodextral
@JanSchultke It's the type that E2 would have after these conversions. E2 is the prvalue with type S in this case, not the const S xvalueLeukocyte
@Sinistrodextral There are tonnes of places in the standard where the lvalue-to-rvalue is applied to things that could be glvalues or prvalues. Nothing happens if it's already a prvalue. The array-to-pointer conversion is also applied to a not-array, nothing also happens with that tooLeukocyte
@JanSchultke As Artyer said, after conversion, E2 is the prvalue with type S. So the result should be an xvalue of type const S. Then decltype will produce const S&&, so MSVC is correct?Sinistrodextral
@Leukocyte the way I read it, there is a conversion from E1 f(), which is of type const S to a type related to the target type T2, S. However, I fail to see the reason why this conversion would drop the cv-qualifier of f(), which doesn't generally happen; only for fundamental types.Cinquefoil
@Sinistrodextral You have to read further to figure out what the type of the conditional operator would be. It's only going to be an xvalue if both operands are glvalues of the same type (eel.is/c++draft/expr.cond#5). I don't think any compiler is right in this case, but MSVC is wrong in a different way. It must think that one of the conversions isn't possible or so; perhaps it thinks that only the conversion from g() to const S is possible, and then you end up with an rvalue reference on both sides, so to speak.Cinquefoil
@JanSchultke The conversion is applied to E2 g() with type S, not to E1 f() with type const SSinistrodextral
@JanSchultke The conversion from f() to a type related to S will be formed because const S can not convert to S. So the last second and third operands are both xvalue with type const SSinistrodextral
@Sinistrodextral why would const S not be convertible to S? S has an implicitly-defined (trivial) copy constructor, so when the compiler attempts to form the ICS, this will include a call to that constructor as a user-defined conversion. Also I'm not sure what you mean by those points about E2.Cinquefoil
@JanSchultke You‘re rightSinistrodextral
I looked at P3177R0, and it seemed to indicate that MSVC was wrong, but I also couldn't figure out why this was the case. Maybe this result just illustrates the behavior of GCC/Clang?Sinistrodextral
@Sinistrodextral based on everything I have read and everything I have looked into now (which is quite a lot), I think that the intended and GCC-implemented behavior is to produce const S. However, the wording is defective so no one is really correct; MSVC is just further from the wanted result. It's a closed-source compiler, so who knows why.Cinquefoil

© 2022 - 2024 — McMap. All rights reserved.