I'm going to break it down, word by word, lets see how this goes. Please, fact check this!
#include <type_traits>struct S {};bool b;int main()
{ const S s{}; static_assert(std::is_same<decltype(b ? S{} : s), const S>::value);} Here,
http://eel.is/c++draft/expr.cond#4 applies because S{} and s have differing types (S and const S). Therefore, we must form a implicit conversion sequence between the two:
Given E1 is S{}, E2 is s, T1 is S and T2 is const S:
- E2 is an lvalue, therefore the target type is const S&
http://eel.is/c++draft/expr.cond#4.1. The target is a reference type, so we follow the rules of
http://eel.is/c++draft/over.ics.ref - S{} binds directly to the reference. The conversion is an identity conversion.
http://eel.is/c++draft/over.ics.ref#1 -
http://eel.is/c++draft/expr.cond#4.1 mandates that the reference must bind directly to the glvalue.
This is the first point of divergence: If this is indeed referring to the operand itself, in this case S{}, without the temporary materialization conversion applied, then this sub clause does not apply, and instead the rules of
http://eel.is/c++draft/expr.cond#4.3 apply, which state that in this case, since T2 is al least as cv-qualified as T1, the target type is T2, and a conversion sequence is formed; the result is the same for both. However, this would stop us dead in our tracks, and make the program ill-formed.
I don't believe this is intended. We will assume this is unintentional, and throw in the possibility that the conversion sequence cannot be formed, assuming that
http://eel.is/c++draft/expr.cond#4.1 really does require E1 to be a glvalue without the temporary materialization conversion applied.
Now, the calculation is complete.
The conversion sequence for S{} to s is either not able to be formed, or is an identity conversion.Now, for the second sequence:
Given E1 is s, E2 is S{}, T1 is const S and T2 is S:
- E2 is a prvalue so
http://eel.is/c++draft/expr.cond#4.3 applies. T2 is less cv-qualified than T1, so 4.3.1 does not apply. T2 is not a base class, so 4.3.2 does not apply. Therefore, the target type is the type of E2 after the lvalue-to-rvalue, function-to-pointer, and array to pointer conversions are applied - these have no effect. The target type is S.
This calculation is complete.
The conversion sequence for s to S{} is an identity conversion. Here are our two options now:
1. The conversion sequence for S{} to s cannot be formed, so the conversion sequence for s to S{} is applied to s
http://eel.is/c++draft/expr.cond#4.sentence-7. It has no effect on value category or type, as no conversion is performed.
2. Both conversion sequences are formed, making the program ill-formed.
http://eel.is/c++draft/expr.cond#4.sentence-5Since option 2 makes the program ill-formed, and the big 4 all agree that this is a well-formed construct, we can assume that it does mean that in the case of
https://eel.is/c++draft/expr.cond#4.1 E1 must be a glvalue, the reference must bind directly to it, and
http://eel.is/c++draft/expr.cond#4.3 will not kick in to form that implicit conversion sequence.
From this point on, we will refer to the converted operands as CE1, and CE2 (even though only an identity conversion was done)
Now for option 1:
- EC1 and EC2 do not have the same value category, nor the same type, so
http://eel.is/c++draft/expr.cond#5 does not apply. This means the result is a prvalue.
http://eel.is/c++draft/expr.cond#6- Since the types of EC1 and EC2 differ, and both are class types,
http://eel.is/c++draft/expr.cond#6 say "overload resolution is used to determine the conversions (if any) to be applied to the operands" and cross-references
http://eel.is/c++draft/over.match.oper#3.3 and
http://eel.is/c++draft/over.match.oper.
This is the second point of divergence, built in candidates are defined for the conditional operator, but since it is not a binary operator nor a unary operator, the entirety of
http://eel.is/c++draft/over.match.oper#3 does not apply. The note
http://eel.is/c++draft/over.match.oper#1.sentence-3 does state that the rules in the sub-clause are used to determine the conversions applied to the operands, but there is no such clause in
http://eel.is/c++draft/over.match.oper that describes this behavior in our case (nor even a semblance of one).
http://eel.is/c++draft/over.match.oper#2 says that overload resolution is performed to determine which built-in operator will be used. However, the only candidates defined (
http://eel.is/c++draft/over.built#27 and
http://eel.is/c++draft/over.built#28) do not include class types, and since S cannot be converted to an arithmetic type, pointer type, pointer to member type or scoped enumeration type, none of these will be selected.
This is definitely not intended, since it would only work for class types convertible to arithmetic, pointer, pointer to member and scoped enumeration types.
WARNING: HERE BE SPECULATION LAND
This leaves us with two options, the construct is ill-formed, or we can assume make an educated guess based on the text. Since all compilers tested accept this, we can assume that there is some invented built in candidate
T operator?:(bool, T, T); Where T is determined as follows.
- If both operands are of the same class type (ignoring cv-qualification), the T is the cv-combined type of the types of both operands- Otherwise, if only one operand is of (possibly cv-qualified) class type U, T is U
- Otherwise, if both are class types, an attempt is made to form conversion sequences between the two. If both are formed, or none are, the program is ill-formed. Otherwise, T is the target type of the conversion sequence that was formed.
(This is by no means correct, just a rough outline based on my observations of the behavior of clang and gcc. This should replicated the behavior, but I have not tested all possible cases)
This would result in overload resolution selecting this invented operator function, and the operands would be converted to the types of the parameters
http://eel.is/c++draft/over.match.oper#10.
When applied to our case, the converted operands are of type S and const S; the cv-combined type of these is const S, leading both operands being converted to const S.
Now that the types match exactly,
http://eel.is/c++draft/expr.cond#7 applies, and the lvalue-to-rvalue, array-to-pointer, and function-to-pointer standard conversions are performed on the second and third operands, and the result is of type const S, which is what clang and gcc report.
Here is the final tally of possible behaviors:
1. Both conversion sequences are formed, making the program ill-formed (I don't consider this an option, I just included it here for the sake of being complete)
2. Only the s to S{} sequence is formed, but no built in candidate exists for T operator?:(T, T) where T is a class type. Overload resolution fails, and the program is ill formed.
3. Only the s to S{} sequence is formed, and there exists a candidate function T operator?:(T, T) where T is const S. Overload resolution is successful, and both operands are converted to const S. The expression is a prvalue of type const S.
Option 3 is the one that best illustrates the behavior of the major compilers, so even though it uses some extrapolated wording, I believe this is what is actually happening. After spending 2.5 hours researching this and tracing it though the standard, I think its safe to say that this wording is defective. I think that the fact that top level cv-qualifiers are ignored when forming a implicit conversion sequence was overlooked when this was written, or perhaps the wording was changed, and this was just never updated.