C++ Logo

std-discussion

Advanced search

Re: Conditional operator with const lvalue and non-const prvalue

From: Krystian Stasiowski <sdkrystian_at_[hidden]>
Date: Tue, 24 Sep 2019 05:42:25 -0400
Oops. Here it is again: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.- The target type is not a reference, so http://eel.is/c++draft/over.best.ics#6 applies. The conversion sequence is the one that converts E2 to a prvalue of of target type, S http://eel.is/c++draft/over.best.ics#6.sentence-2. Top level cv-qualifiers are ignored http://eel.is/c++draft/over.best.ics#6.sentence-4. As they are ignored, and they both have the same class type, the conversion is an identity conversion http://eel.is/c++draft/over.best.ics#6.sentence-7Side note: The standard kinda disagrees with itself here, it first states "The implicit conversion sequence is the one required to convert the argument expression to a prvalue of the type of the parameter." (http://eel.is/c++draft/over.best.ics#6.sentence-2) and also says "When the parameter has a class type and the argument expression has the same type, the implicit conversion sequence is an identity conversion.". We will assume that it means the later since it is more constrained, and therefore does not include the lvalue-to-rvalue conversion. This does not change the later result, as a lvalue-to-rvalue conversion would not discard the cv-qualifiers https://eel.is/c++draft/conv.lval#1This 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 LANDThis 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.
-------- Original message --------From: Andrew Schepler via Std-Discussion <std-discussion_at_[hidden]> Date: 9/24/19 05:21 (GMT-05:00) To: std-discussion_at_[hidden] Cc: Andrew Schepler <aschepler_at_[hidden]> Subject: Re: [std-discussion] Conditional operator with const lvalue and non-const prvalue It looks like you accidentally sent that last message to just me, and not the mailing list:On Tue, Sep 24, 2019 at 12:29 AM Krystian Stasiowski <sdkrystian_at_[hidden]> wrote: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.- The target type is not a reference, so http://eel.is/c++draft/over.best.ics#6 applies. The conversion sequence is the one that converts E2 to a prvalue of of target type, S http://eel.is/c++draft/over.best.ics#6.sentence-2. Top level cv-qualifiers are ignored http://eel.is/c++draft/over.best.ics#6.sentence-4. As they are ignored, and they both have the same class type, the conversion is an identity conversion http://eel.is/c++draft/over.best.ics#6.sentence-7Side note: The standard kinda disagrees with itself here, it first states "The implicit conversion sequence is the one required to convert the argument expression to a prvalue of the type of the parameter." (http://eel.is/c++draft/over.best.ics#6.sentence-2) and also says "When the parameter has a class type and the argument expression has the same type, the implicit conversion sequence is an identity conversion.". We will assume that it means the later since it is more constrained, and therefore does not include the lvalue-to-rvalue conversion. This does not change the later result, as a lvalue-to-rvalue conversion would not discard the cv-qualifiers https://eel.is/c++draft/conv.lval#1This 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 LANDThis 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.On Mon, Sep 23, 2019 at 7:42 PM Andrew Schepler <aschepler_at_[hidden]> wrote:Yes, the conversion from prvalue S{} to type const S& would bind directly, but it "is said to bind directly to the initializer expression", and the initializer expression "S{}" is not a glvalue. A bit confusing, since the reference "binds to" the lvalue resulting from the temporary materialization conversion. But I think this is intended, because I think the point is that a temporary materialization conversion is never applied to just one of the second or third operands of the conditional. If it did, automatically destroying the temporary at the end of its lifetime would depend on more than just the program counter register, causing brand new complications for compilers, particularly for stack unwinding on exception propagation.There certainly is an implicit conversion sequence from an lvalue of type const S to type S, via the copy constructor. Otherwise we couldn't do:struct S {};void f(S) {}int main() { const S s; f(s);}But in converting "S{}" to a type related to "s", wouldn't [expr.cond]/(4.3) apply, if the conversion in [expr.cond]/(4.1) is not possible? Note there have been changes to (4.3) since C++17:C++17 N4659:(4.3.1) if T1 and T2 are the same class type (ignoring cv-qualification), or one is a base class of the other, and T2 is at least as cv-qualified as T1, the target type is T2,(4.3.2) otherwise, ...C++20 latest at https://timsong-cpp.github.io/cppwp/expr.cond :(4.3.1) if T1 and T2 are the same class type (ignoring cv-qualification) and T2 is at least as cv-qualified as T1, the target type is T2,(4.3.2) otherwise, if T2 is a base class of T1, the target type is cv1 T2, where cv1 denotes the cv-qualifiers of T1,(4.3.3) otherwise, ...But I don't see how that change would make any implicit conversion sequence unavailable. Which would mean both are possible and "b ? S{} : s" is ill-formed - not desirable. (It might make sense to say if both are possible and the target types are the same, the expression is a prvalue with that type; but it doesn't say that.)On Mon, Sep 23, 2019 at 6:51 PM Krystian Stasiowski via Std-Discussion <std-discussion_at_[hidden]> wrote:Only the last case where the initializer expression is converted does it not bind directly

http://eel.is/c++draft/dcl.init.ref#5.4The implicit conversion sequence can be formed from the prvalue to the lvalue. Since constness matters for prvalues of class type and is not ignored, an implicit conversion sequence cannot be formed from const S to S. I think.On Mon, Sep 23, 2019 at 11:02 AM Brian Bi via Std-Discussion <std-discussion_at_[hidden]> wrote:Consider the following:#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 GCC and Clang confirm that the type is correct. But I can't figure out why. My reading of the standard is that the result should be a prvalue of type S, not const S.[expr.cond]/4 applies because the two operand types are not the same (one is S, the other is const S) but at least one is a class type. We must therefore try to form an implicit conversion sequence in each direction. An implicit conversion sequence cannot be formed from the prvalue operand to const S& because (4.1) contains a restriction that the reference must bind directly to a glvalue. In the other direction, we have the identity conversion sequence from the const lvalue operand to S. Thus, /4 seems to tell us that the const lvalue operand must be converted to S, and the result should have type S.Yet I would not expect both GCC and Clang to be wrong here, so I think that I have misunderstood the standard in this case. Surely there must be a reason why the result has type const S, but I can't figure it out.(Even if we assume that the compiler is obligated to perform an lvalue-to-rvalue conversion on the const lvalue operand, resulting in a const prvalue, that still doesn't seem to explain the result. If this were the case, /4 would end with a const prvalue and a non-const prvalue, /5 would not apply, and we would get to /6 and the types would still not be the same. The "overload resolution" procedure prescribed there would fail since S can't be converted to any scalar types, making the program ill-formed. This interpretation thus cannot be correct either.)-- Brian Bi
-- 
Std-Discussion mailing list
Std-Discussion_at_[hidden]
https://lists.isocpp.org/mailman/listinfo.cgi/std-discussion
-- 
Std-Discussion mailing list
Std-Discussion_at_[hidden]
https://lists.isocpp.org/mailman/listinfo.cgi/std-discussion"Identity conversion" does not mean there is no change in type or value category. It only means that the conversion sequence is associated with category "Identity conversion" for purposes of comparing which implicit conversion sequence is better during overload resolution. In [over.best.ics]/6, "Any difference in top-level cv-qualification is subsumed by the initialization itself and does not constitute a conversion." doesn't mean that the cv-qualifiers of the target type must match the expression type; it means that if the only difference in qualifiers is at the top level, the implicit conversion sequence used in comparisons doesn't include a qualification conversion. In the same paragraph, "When the parameter has a class type and the argument expression has the same type, the implicit conversion sequence is an identity conversion." also doesn't change the target type; it means that even if the actual initialization requires a move/copy constructor, the ICS category is Identity and the rank is Exact Match.So the conversion of "s" to a type related to "S{}" can be from "const S" to "S". If we suppose that the conversion from "S{}" to a type related to "s" cannot be formed, I agree the final type should be "S" and not "const S". Though I still don't see why (4.3) wouldn't apply to the conversion of "S{}" if (4.1) doesn't. Possibly the Standard wording there could be fixed or clarified.I also agree there seems to be some wording missing about exactly when and how overload resolution is involved in semantic analysis of the conditional expression. Something should say that it goes through the same process as unary and binary expressions in [over.match.oper] using the phony name "operator?:", but the only candidate functions are the built-in candidate set. But I think it's intentional that [over.built] does not include any signatures involving class types, because getting past [expr.cond]/5 to the "Otherwise" [expr.cond]/6 where overload resolution gets involved is meant to be just for cases where the types aren't similar enough to use "Exact Match" or "Derived To Base" conversion sequences, and we can't convert either expression to the other's type, so the final option is to use one or two conversion functions to convert each class type operand to a non-class type.Though this is backwards from the normal pattern: for most compound expressions, we can start with [over.match.oper]. At the very start, it says overloading does not apply if no operand has class or enum type, so only [expr] applies. Otherwise, overload resolution determines whether the compound expression is a function call or has the "built-in" meaning in [expr], and if it has the built-in meaning, the selected built-in candidate function determines any conversions to be applied before the semantics of [expr]. For a ternary conditional expression, it only makes sense to start analysis at [expr.cond]. If we pass the "Otherwise" at the start of [expr.cond], then [over.match.oper] applies, forcing the result type to be a non-class type. If that's really how it should be read, I think it would be good to have something mention that exception.(Some of your phrasing seems to imply "since compilers do this, the Standard must be interpreted in a way consistent with that result", and I'd be careful about that reversal. Compiler teams are capable of error, and there might be some common reason multiple implementations use logic that matches the Standard most of the time but happens to give a consistent different result in one corner case not specifically intended. There's also the possibility the Standard has a defect or insufficiently clear wording. And of course common sense can enter into it: if selecting one of two expressions with a type differing only by "const" can ever make the code ill-formed, something is wrong.)

Received on 2019-09-24 04:44:39