C++ Logo


Advanced search

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

From: Andrew Schepler <aschepler_at_[hidden]>
Date: Tue, 24 Sep 2019 05:21:47 -0400
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]>

> 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-7
> Side 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#1
> 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-5
> Since 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.4
>>> The 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 <http://coliru.stacked-crooked.com/a/49d1853146cbcdc1> 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

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:24:10