Skip to content

CWG3000 [over.match.oper] Conditional operator is not handled despite false claim in note #679

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Eisenwave opened this issue Mar 5, 2025 · 20 comments

Comments

@Eisenwave
Copy link
Collaborator

Reference (section label): [over.match.oper]

Link to reflector thread (if any): https://lists.isocpp.org/core/current/17459.php

Issue description

[over.match.operator] Note 1 states:

?: cannot be overloaded, but the rules in this subclause are used to determine the conversions to be applied to the second and third operands when they have class or enumeration type ([expr.cond]).

However, there exist no rules in [over.match.oper] which apply to the conditional operator. These rules are needed when overload resolution is requested by [expr.cond] paragraph 6.

Suggested resolution

Append a row to [tab:over.match.oper] with the following contents:

Subclause Expression As member function As non-member function
a?b:c operator?:(/* see below */,b,c)

Underneath [tab:over.match.oper], within the same paragraph, append the following:

The first operand of operator?: is a prvalue of type bool whose value is the result of contextually converting the first operand of the conditional operator to bool prior to overload resolution ([expr.cond])

Editor's note: The intent is that for a ? b : c, an operator bool for the condition is only meant to be evaluated once. [expr.cond] paragraph 1 already performs such contextual conversion, and we wouldn't want to evaluate operator bool a second time just because we fall into [expr.cond] p6. Alternatively, we could remove the bool parameter in [over.built] and translate a?b:c into operator?:(b,c) for the purpose of overload resolution.

Change [over.match.oper] paragraph 3 as follows:

For a unary operator @ with an operand of type cv1 T1, and for a binary operator @ with a left operand of type cv1 T1 and a right operand of type cv2 T2, and for the conditional operator, four sets of candidate functions, designated member candidates, non-member candidates, built-in candidates, and rewritten candidates, are constructed as follows:

Editor's note: Specifying cv1 T1 etc. in this case is not necessary because the third bullet applies to conditional operator, and the third bullet does not use these definitions.

Further notes

Even if there were rules to translate a?b:c into operator?:(a,b,c), [over.built] has no built-in candidate for operator?: that accepts class types. Therefore, the resolution of CWG2321 is incomplete (see linked reflector), but that is a separate defect (discovered by Brian Bi).

@Eisenwave
Copy link
Collaborator Author

Note that Note 1 is ancient C++98 wording. From what I can tell, the note has been wrong from the start and no one noticed until now.

See https://www.open-std.org/jtc1/sc22/wg21/docs/wp/html/nov97-2/

@Eisenwave
Copy link
Collaborator Author

CC @t3nsor

@jensmaurer
Copy link
Member

I'm not sure we need [expr.cond] p6 anymore, given all the implicit conversions applied in p4. Any example to the contrary?

@Eisenwave
Copy link
Collaborator Author

Eisenwave commented Mar 5, 2025

@jensmaurer see the reflector discussion. We run into a case with the example in CWG2321 where one side is const T and the other is T for a class type T.

I'm not sure if there are other cases, and even this one may be unintentional. I also would be opposed to removing this whole overload resolution mechanism and explaining what happens directly in p6. It's pretty convoluted for what it does.

@jensmaurer
Copy link
Member

We can massage p7.1 to ignore const differences (see my reflector message).

@jensmaurer jensmaurer changed the title [over.match.oper] Conditional operator is not handled despite false claim in note CWG3000 [over.match.oper] Conditional operator is not handled despite false claim in note Mar 5, 2025
@jensmaurer
Copy link
Member

CWG3000 fixes this by removing the overload resolution rule.

@lprv
Copy link

lprv commented Mar 5, 2025

The purpose of [expr.cond]/6 is to make the following work:

struct X { operator int() { return 1; } };
struct Y { operator int() { return 2; } };
int i = false ? X() : Y();
assert(i == 2);

Removing the paragraph outright seems misguided.

For CWG2321, I think the correct fix would be to apply the conversion at the end of p4 to both operands:

  • Otherwise, if exactly one conversion sequence can be formed, that conversion is applied to the chosen operand both operands are implicitly converted to its target type and the converted operands is are used in place of the original operands for the remainder of this subclause.

The issue pointed out by the OP is that [expr.cond]/6 says "overload resolution is performed" with a cross-reference to [over.match.oper], but the latter does not actually tell us what that means (i.e. what the candidates are). I think inserting a new paragraph somewhere in [over.match.oper] addressing this case should be sufficient; we don't need to group it with the other operators since it's special and can't be overloaded.

For the conditional operator, overload resolution is performed only as described in 7.6.16 [expr.cond]; the candidate functions are the built-in candidates (12.5 [over.built]) named operator?:.

@jensmaurer
Copy link
Member

CWG3000 has been updated to address a bag of concerns.

@lprv
Copy link

lprv commented Mar 8, 2025

Thanks. The new P/R still has some issues:

  • The "similar"/"qualification-combined" terminology used for the determination of the combined target type at the end of p4 does not work for reference types introduced in the first two bullets of the paragraph. (For example, int& and const int& are not similar, and their qualification-combined type is just int&.)

  • "Qualification-combined type" is not suitable for merging top-level cv-qualifiers in general: the current definition of the term ignores them.

  • The change to the target type in p4 unintentionally affects user-defined conversions, for example:

    struct X {};
    struct Y {
        operator X&() const;       // #1
        operator const X&() const; // #2
    };
    X x;
    const Y y;
    auto& xy = 0 ? x : y;

    This currently calls #1 and yields an lvalue of type X, the proposed change adds a const to the target type which would call #2 and yield a const X.

  • I'm still unsure about mixing operator?: with the other operators in [over.match.oper]. This puts it in the purview of [over.match.oper] paragraphs 2 and 11, suggesting that conditional expressions (with class or enumeration operands) undergo overload resolution first, and then the converted operands are sent to [expr.cond], as with the other operators. This would not be a correct reading: we want overload resolution for operator?: to be performed only as explicitly directed by [expr.cond] p6.

On a related note, the resolution of CWG2865 also seems wrong: the intent (per the description of the issue and the discussion in #442) was to have the conditional operator with operands "lvalue of type const X" and "prvalue of type X" (for a class type X) yield "prvalue of type const X", but the actual change made the result an lvalue.

Here's an alternative wording suggestion for the updated CWG3000:

Edit 7.2.2 [expr.type] p3:

The composite pointer type of two operands p1 and p2 having types T1 and T2, respectively, where at least one is a pointer or pointer-to-member type or std::nullptr_t, is:

  • [...]
  • if T1 is "pointer to cv1 C1" and T2 is "pointer to cv2 C2", where C1 is reference-related to C2 or C2 is reference-related to C1 (9.4.4 [dcl.init.ref]), "pointer to C", where C is the qualification-combined type (7.3.6 [conv.qual]) of T1 C1 and T2 C2 or the qualification-combined type of T2 C2 and T1 C1, respectively;
  • [...]
  • if T1 is "pointer to member of C1 of type cv1 U" and T2 is "pointer to member of C2 of type cv2 U" ...
  • if T1 and T2 are similar types ...
  • if T1 is "pointer to member of C1 of type U1" and T2 is "pointer to member of C2 of type U2", U1 and U2 are similar (7.3.6 [conv.qual]), and either C1 and C2 are the same type or one is a base class of the other, "pointer to member of C of type U", where:
    • C is C1 if C2 is a base class of C1 and C2 otherwise, and
    • U is the qualification-combined type of U1 and U2;

Edit 7.3.6 [conv.qual] p3:

The qualification-combined type of two types T1 and T2 is the type T3 similar to T1 whose longest qualification-decomposition is such that:

  • for every i > 0, cvi3 is the union of cvi1 and cvi2,
  • [...]
  • if the resulting cvi3 is different from cvi1 or cvi2, or the resulting Pi3 is different from Pi1 or Pi2, then const is added to every cvk3 for 0 < k < i,

where cvij and cvij are the components of the longest qualification-decomposition of Tj. A prvalue of type T1 can be converted to type T2 if:

  • T1 is "pointer to U1" and T2 is "pointer to U2", or
  • T1 is "pointer to member of C of type U1" and T2 is "pointer to member of C of type U2",

and the qualification-combined type of T1 U1 and T2 U2 is T2 U2. The value is unchanged by this conversion.

Edit 7.6.16 [expr.cond] p3:

Otherwise, if the second and third operands are glvalue bit-fields of the same value category and of similar types cv1 T T1 and cv2 T T2 (7.3.6 [conv.qual]), respectively, the operands are considered to be of type cv T C for the remainder of this subclause, where cv C is the union of cv1 and cv2 qualification-combined type of T1 and T2.

Edit 7.6.16 [expr.cond] p4:

Otherwise, if at least one of the second and third operands have different types and either has (possibly cv-qualified) class type, or if both are glvalues of the same value category and the same type except for cv-qualification, an attempt is made to form an implicit conversion sequence (12.2.4.2 [over.best.ics]) from each of those operands to a type related to the type of the other.
[...]

  • If E2 is an lvalue a glvalue, the target type is "lvalue reference to T2 T3", but where:
    • the reference is an lvalue reference if E2 is an lvalue and an rvalue reference otherwise;
    • the type T3 is determined as follows:
      • if T2 is reference-related to T1 (9.4.4 [dcl.init.ref]), T3 is the qualification-combined type of T2 and T1 (7.3.6 [conv.qual]),
      • otherwise, T3 is T2; and
    • an implicit conversion sequence can only be formed if the reference would bind directly (9.4.4 [dcl.init.ref]) to a glvalue.
  • If E2 is an xvalue ...
  • If E2 is a prvalue or if neither of the conversion sequences above cannot be formed and at least one of the operands has (possibly cv-qualified) class type:
    • if T1 and T2 are the same class type (ignoring cv-qualification):
      • if T2 is at least as cv-qualified as T1, the target type is T2,
      • otherwise, and either E1 is a prvalue or E2 is an lvalue, no conversion sequence can be formed for this operand;
    • otherwise, if T2 is a base class of reference-related to T1, the target type is cv1 T2, where cv1 denotes the cv-qualifiers of T1 the qualification-combined type of T2 and T1;
    • otherwise, the target type is the type that E2 would have after applying the lvalue-to-rvalue (7.3.2 [conv.lval]), array-to-pointer (7.3.3 [conv.array]), and function-to-pointer (7.3.4 [conv.func]) standard conversions.

Using this process, it is determined whether an implicit conversion sequence can be formed from the second operand to the target type determined for the third operand, and vice versa, with the following outcome:

  • If both sequences can be formed ...

If no conversion sequence can be formed, the operands are left unchanged and further checking is performed as described below. Otherwise, a common type C is determined as follows:

  • If exactly one conversion sequence can be formed, C is the target type of that conversion sequence.
  • Otherwise, if the second and third operands have the same type (ignoring cv-qualification), exactly one of the conversions will have a target type that is not a reference type [Footnote: This property is ensured by the definition given above. —end footnote]; C is that type.
  • Otherwise, the program is ill-formed.
  • Otherwise, if exactly one conversion sequence can be formed ...

The second and third operands are implicitly converted to the type C, and the converted operands are used in place of the original operands for the remainder of this subclause.

Add a new paragraph to 12.2.2.3 [over.match.oper]:

For the conditional operator, overload resolution is performed only as described in 7.6.16 [expr.cond]; the candidate functions are the built-in candidates (12.5 [over.built]) named operator?:.


The changes can be summarized thusly (with the help of COND-RES from [meta.trans.other]):

  • "Qualification-combined type" is modified to also take top-level cv-qualifiers into account in order to make it suitable for computing the "composite type" of things other than pointers; existing uses of the term are adjusted to account for the change. (This partially addresses CWG2438.)
  • When the second and third operands of the conditional operator have the same value category and similar types, they are brought to the "composite type" mentioned above (this addresses CWG2023, I think):
    • COND-RES(int&, const int&) is const int& (status quo),
    • COND-RES(const int&, volatile int&) is const volatile int& (currently int),
    • COND-RES(int*&, const int*&) is const int* const& (currently const int*),
    • COND-RES(const X, volatile X) for a class X is const volatile X (currently ill-formed).
  • When forming an implicit conversion sequence in p4 from an expression of a derived class type to a reference to a base class type, the cv-qualifiers of the operands are merged:
    • COND-RES(Base&, const Derived&) is const Base& (currently ill-formed).
  • When the second and third operands are of similar class types but different value categories, implicit conversions are attempted in both directions (same as now), but the result is a prvalue whenever possible:
    • COND-RES(cv1 X&, cv2 X) for a class X is:

      • cv12 X if the second operand is convertible to that type,
      • otherwise, cv12 X& if the third operand is convertible to that type,
      • otherwise, no conversions are applied in p4,

      and similarly for COND-RES(cv1 X&&, cv2 X);

    • COND-RES(cv1 X&, cv2 X&&) is the same as COND-RES(cv1 X&, cv2 X).

  • As a drive-by fix, the definition of "composite pointer type" for member pointers is adjusted to allow COND-RES(int* Base::*, const int* Derived::*) to yield the expected type const int* const Derived::* instead of being ill-formed. (Both COND-RES(int Base::*, const int Derived::*) and COND-RES(int* Base::*, const int* Base::*) already work.)

Notably, this preserves the questionable status quo that expressions converted to non-reference types sometimes needlessly propagate their original cv-qualification into the type of the resulting prvalue (e.g. COND-RES(const X&, X) is const X and not just X, COND-RES(Base, const Derived) is const Base and not just Base). This is something P3177 aims to fix, which would require a minimal change to the above wording (in [expr.cond] p4):

  • If E2 is a prvalue or if ...
    • [...]
    • otherwise, if T2 is reference-related to ...
    • otherwise, if E2 is a prvalue, the target type is T2;
    • otherwise, the target type is the cv-unqualified version of the type that E2 would have ...

@jensmaurer
Copy link
Member

jensmaurer commented Mar 8, 2025

We shall not use meta-stuff defined in the library section for core language definitions, ever.

I understand the edits using "qualification-combined" in the existing scenarios are wrong, because they aim to copy top-level cv-qualification, but "qualification-combined" doesn't. I'm wondering whether we should just revert that part, instead of changing the definition of "qualification-combined".

@lprv
Copy link

lprv commented Mar 8, 2025

We shall not use meta-stuff defined in the library section for core language definitions, ever.

The proposed wording does not use it. It only appears in the (informal) summary of what the proposed wording does, as a convenient shortcut for "the result of the conditional operator with the operands ..."

I've un-collapsed the proposed wording to make it clearer.

@jensmaurer
Copy link
Member

So, I think we should leave expr.type and conv.qual mostly alone, adding just "longest" and "The value is unchanged" to the latter (which are simple drive-by fixes).

If there is something wrong with composite pointer type, that should be a separate core issue.

@jensmaurer
Copy link
Member

Let's take the rest piecemeal. First, the [over.match.oper] fix looks mostly good; we still need to map the operator syntax to a (hypothetical) function call to make the connection to [over.built]. Done in CWG3000 (at the bottom).

@jensmaurer
Copy link
Member

The "similar"/"qualification-combined" terminology used for the determination of the combined target type at the end of p4 does not work for reference types introduced in the first two bullets of the paragraph. (For example, int& and const int& are not similar, and their qualification-combined type is just int&.)

For the first two bullets of the (current p4), I don't think we'll ever have different (but similar) target types when the conversion works both ways.

@jensmaurer
Copy link
Member

On a related note, the resolution of CWG2865 also seems wrong: the intent (per the description of the issue and the discussion in #442) was to have the conditional operator with operands "lvalue of type const X" and "prvalue of type X" (for a class type X) yield "prvalue of type const X", but the actual change made the result an lvalue.

Hm... If E2 is the "prvalue of type X", we don't form a conversion sequence per the text added by CWG2865.
If E2 is "lvalue of type const X" (the other direction), we get to the first bullet, and try to convert "prvalue of type X" to "lvalue reference to const X", which works and binds directly.

@jensmaurer
Copy link
Member

Hm... I'm not seeing how the suggested resolution handles "true ? T() : T()" (should simply yield a prvalue of type T) correctly. It seems we form no conversion sequence at all.

@lprv
Copy link

lprv commented Mar 9, 2025

So, I think we should leave expr.type and conv.qual mostly alone, adding just "longest" and "The value is unchanged" to the latter (which are simple drive-by fixes).

Okay, but then we need another way to compute the composite result type to make the example with pointers from CWG3000 work. This would probably involve using "qualification-combined type" in some form anyway, perhaps by inventing some pointer types and talking about the cv-combined type of those... which doesn't seem ideal (but we already do something similar for "reference-compatible", so maybe it's not that bad). Changing "qualification-combined type" feels like a better approach to me; it's only used in two places at the moment.

For the first two bullets of the (current p4), I don't think we'll ever have different (but similar) target types when the conversion works both ways.

Yes, that's true in the current version. I got confused there; sorry.

Hm... If E2 is the "prvalue of type X", we don't form a conversion sequence per the text added by CWG2865.
If E2 is "lvalue of type const X" (the other direction), we get to the first bullet, and try to convert "prvalue of type X" to "lvalue reference to const X", which works and binds directly.

I agree. This produces an lvalue, but before CWG1895 the result was a prvalue, and the ostensible goal of CWG2865 was to restore that. (All major implementations produce a prvalue.)

Hm... I'm not seeing how the suggested resolution handles "true ? T() : T()" (should simply yield a prvalue of type T) correctly. It seems we form no conversion sequence at all.

This case is handled by p3: it merges the cv-qualifiers of the second and third operands (a no-op in this case). We then skip p4 (because of the "Otherwise, ..."), and eventually reach p7.1 as expected.

@jensmaurer
Copy link
Member

jensmaurer commented Mar 9, 2025

I've updated CWG3000; two main themes:

  • use reference-related twice to cover "similar" and "derived-to-base"
  • don't be afraid of both conversions working, clean up afterwards

@lprv
Copy link

lprv commented Mar 10, 2025

In the case where neither operand has class type, the first conversion of p4 should always work in both directions, so I don't think we need this part anymore:

  • If E2 is a prvalue or if the conversion sequence above cannot be formed and at least one of the operands has (possibly cv-qualified) class type:

I realized after initially sending this reply that, per CWG2898, it is intended that an implicit conversion sequence from cv1 T to cv2 T can always be formed, even if T has no appropriate constructor. If that reading is accurate, the remaining suggestions can be ignored, as they only make sense under the opposite interpretation. Otherwise, click to expand:

Suggestions possibly voided by CWG2898

I would additionally suggest removing "second and third operand have different types" from p4. Consider:

struct X {
    X();
    X(X&);
};
using CX = const X;
CX cx;

auto r1 = true ? X() : cx;
auto r2 = true ? CX() : cx;
  • For r1, we try to convert X() to const X& (which works) and cx to const X (which does not); the result is an lvalue. Good.
  • r2 looks completely analogous to r1 (the target types for the implicit conversion sequences would be the same), but because the operands already have the same type we never try the conversions and go directly to p6, which makes the result a prvalue (so r2 is ill-formed).

Otherwise, if at least one of the second and third operands have different types and either has (possibly cv-qualified) class type, or if ...


Also consider:

struct X {
    X();
    X(X&);
    operator int() volatile;
};
volatile X vx;

auto r3 = true ? static_cast<X&&>(X()) : vx;
auto r4 = true ? static_cast<volatile X&&>(vx) : vx;

None of the p4 conversions work in this example. For r3, this means that we reach p6 and do the overload resolution step; this works and the result is an int. For r4, overload resolution is not done, once again because the types are already the same, which makes the expression is ill-formed. So perhaps p6 could use a similar adjustment (assuming the previous suggestion is applied):

Otherwise, the result is a prvalue. If the second and third operands do not have the same type and value category, and ...


There's another (preexisting) issue with p4. I'm not sure whether that's something that should be addressed as part of this issue (or at all), but I'll mention it here for completeness.

When attempting to form an implicit conversion sequence from E1 to a type related to T2, we never check if E2 is also convertible to that type. (This only affects the third (now second) bullet of p4, and only when E2 is a glvalue.) For example:

struct X {
    operator int() volatile;
};
volatile X vx;

auto r5 = true ? vx : X();

vx is not convertible to volatile X, so no conversion is formed in that direction. X() is not convertible to volatile X& but the conversion to volatile X works, so we take that as the target type despite the fact that vx is not convertible to it, making the expression ill-formed.

We could say that the the ICS in p4 can be formed only if both operands are convertible to its target type, which would allow r5 to reach the overload resolution step with the original operands and yield int.

@jensmaurer
Copy link
Member

In the case where neither operand has class type, the first conversion of p4 should always work in both directions, so I don't think we need this part anymore:

Removed. See CWG3000.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants