Skip to content

CWG2844 [over.match.oper] The algorithm for enumerating a finite set of built-in candidates is underspecified #362

Description

@t3nsor

Full name of submitter: Brian Bi

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

Issue description: Consider the following example, which is accepted by Clang, but rejected by GCC:

#include <concepts>

struct S1 {
    operator int*() { return nullptr; }
};

struct S2 {
    template <class T>
    operator T() requires std::same_as<T, int*> {
        return nullptr;
    }
};

int main() {
    S1 s1;
    S2 s2;
    return s1 - s2;
}

The question is whether the implementation is required to find the built-in candidate std::ptrdiff_t operator-(int*, int*), and select that candidate. The problem is that [over.built] specifies that there are an infinite number of built-in candidates, including one of the form std::ptrdiff_t operator-(T*, T*) for every object type T. If there are infinitely many built-in candidates, the implementation cannot iterate through all of them to determine whether each candidate is viable.

One plausible approach is: look for non-template conversion functions in each operand of class type. If at least one operand yields a non-empty list of such non-template conversion functions, then consider the built-in candidates that have parameters of types that result from those conversion functions (or can be converted from one of those resulting types by a standard conversion). Under this approach, the code should be accepted. If this is the algorithm we want, we should specify it normatively so that GCC can also implement it.

[over.match.oper]/3.3 restricts the built-in candidate set to those that "accept operand types to which the given operand or operands can be converted according to [over.best.ics]". We can specify the algorithm normatively by restricting this set further.

Suggested resolution: Add a bullet before [over.match.oper]/3.3:

  • [...]
  • accept the same number of operands, and
  • have at least one parameter type to which a standard conversion sequence ([over.ics.scs]) exists from either the corresponding operand E or, in the case where E has a class type, any type T specified by a non-template conversion function F ([class.conv.fct]) that is a member of E's class and would be viable ([over.match.viable]) for a call of the form (E).N(), where N is a hypothetical id-expression that names F, and
  • accept operand types to which the given operand or operands can be converted according to [over.best.ics], and
  • [...]

Activity

  1. t3nsor commented on Jul 15, 2023

    @t3nsor
    Author

    Ugh, I realized that my suggested resolution doesn't actually work for other cases, such as T* operator-(T*, std::ptrdiff_t) - when the second operand is convertible to std::ptrdiff_t, it gives you an unbounded candidate set because the first parameter can still be any pointer-to-object type. I think the resolution might have to be a bit more complicated.

  2. t3nsor commented on Jul 15, 2023

    @t3nsor
    Author

    It seems that the algorithm for how to enumerate a finite set of candidates must depend on which operand of each operator has an unbounded set of possible types and, if more than one such operand exists, the relationship between them. Here is a suggested resolution along those lines, also updated to cover the case of operands that are overload sets:

    [over.match.oper]/3.3 is unchanged.

    Insert a new paragraph after [over.built]/3:

    For the purposes of this subclause, a type T is admissible for an operand E if a standard conversion sequence ([over.ics.scs]) exists from E to T. If E has a class type, then T is also admissible for E if E's class has a non-template conversion function F ([class.conv.fct]) that would be viable ([over.match.viable]) for a call of the form (E).N(), where N is a hypothetical id-expression that names F, and a standard conversion sequence to T exists from the type specified by F. If E denotes an overload set ([over.over]), then T is admissible for E if E contains any non-template function for which T is admissible.

    Edit [over.built]/4:

    For every pair (T, vq), where T is a cv-unqualified arithmetic type other than bool or a cv-unqualified pointer to (possibly cv-qualified) object type, there exist candidate operator functions of the form

    vq T& operator++(vq T&);
    T operator++(vq T&, int);
    vq T& operator--(vq T&);
    T operator--(vq T&, int);

    if vq T& is admissible for the operand.

    Edit [over.built]/5:

    For every (possibly cv-qualified) object type T and for every function type T that has neither cv-qualifiers nor a ref-qualifier, there exist candidate operator functions of the form

    T& operator*(T*);

    if T* is admissible for the operand.

    Edit [over.built]/6:

    For every type T such that T* is admissible for the operand, there exist candidate operator functions of the form

    T* operator+(T*);

    Edit [over.built]/9:

    For every quintuple (C1, C2, T, cv1, cv2), where C2 is a class type, C1 is the same type as C2 or is a derived class of C2, and T is an object type or a function type, there exist candidate operator functions of the form

    cv12 T& operator->*(cv1 C1*, cv2 T C2::*);

    where cv12 is the union of cv1 and cv2 , if cv2 T C2::* is admissible for the second operand. The return type is shown for exposition only; see [expr.mptr.oper] for the determination of the operator's result type.

    Edit [over.built]/13, splitting it into two bullets:

    • For every cv-qualified or cv-unqualified object type T there exist candidate operator functions of the form

      T* operator+(T*, std::ptrdiff_t);
      T& operator[](T*, std::ptrdiff_t);
      T* operator-(T*, std::ptrdiff_t);

      if T is admissible for the first operand.

    • For every cv-qualified or cv-unqualified object type T there exist candidate operator functions of the form

      T* operator+(std::ptrdiff_t, T*);
      T& operator[](std::ptrdiff_t, T*);

      if T is admissible for the second operand.

    Edit [over.built]/14:

    For every T, where T is a pointer to object type and is admissible for the left or right operand, there exist candidate operator functions of the form

    std::ptrdiff_t operator-(T, T);

    Edit [over.built]/15. Note that there is a comma added after "enumeration type".

    For every T, where T is an enumeration type , or T is a pointer type that is admissible for the left or right operand, there exist candidate operator functions of the form

    bool operator==(T, T);
    bool operator!=(T, T);
    bool operator<(T, T);
    bool operator>(T, T);
    bool operator<=(T, T);
    bool operator>=(T, T);
    R operator<=>(T, T);

    where R is the result type specified in [expr.spaceship].

    Edit [over.built]/16:

    For every T, where T is a pointer-to-member type and is admissible for the left or right operand, or T is std​::​nullptr_t, there exist candidate operator functions of the form

    bool operator==(T, T);
    bool operator!=(T, T);

    Edit [over.built]/19:

    For every pair (T, vq), where T is any type, there exist candidate operator functions of the form

    T*vq& operator=(T*vq&, T*);

    if T vq& is admissible for the left operand or T* is admissible for the right operand.

    Edit [over.built]/20:

    For every pair (T, vq), where T is an enumeration type, or T is a pointer-to-member type such that vq T& is admissible for the left operand or T is admissible for the right operand, there exist candidate operator functions of the form

    vq T& operator=(vq T&, T);

    Edit [over.built]/21:

    For every pair (T, vq), where T is a cv-qualified or cv-unqualified object type, there exist candidate operator functions of the form

    T*vq& operator+=(T*vq&, std::ptrdiff_t);
    T*vq& operator-=(T*vq&, std::ptrdiff_t);

    if T*vq& is admissible for the left operand.

    Edit [over.built]/25:

    For every type T, where T is a pointer, pointer-to-member, or scoped enumeration type,

    • T is admissible for the second or third operand and is a pointer or pointer-to-member type, or
    • T is a scoped enumeration type,

    there exist candidate operator functions of the form

    T operator?:(bool, T, T);

  3. t3nsor commented on Dec 23, 2023

    @t3nsor
    Author

    bump

  4. jensmaurer commented on Jan 7, 2024

    @jensmaurer
    Member

    CWG2844

    Why are we talking about conversions of the first operand of "operator=", although conversions are not allowed there? Why is operator?: in [over.builtin] at all, given that [expr.cond] describes the conversions rather clearly (and not quite like function parameter conversions to the same type). Also, operator?: cannot be overloaded to start with.

  5. changed the title [-][over.match.oper] The algorithm for enumerating a finite set of built-in candidates is underspecified[/-] [+]CWG2844 [over.match.oper] The algorithm for enumerating a finite set of built-in candidates is underspecified[/+] on Jan 7, 2024
  6. cpplearner commented on Jan 8, 2024

    @cpplearner

    Why is operator?: in [over.builtin] at all, given that [expr.cond] describes the conversions rather clearly (and not quite like function parameter conversions to the same type). Also, operator?: cannot be overloaded to start with.

    IIUC these operator?: candidates are used only by [expr.cond]/6 to determine the conversions to be applied to the operands.

  7. t3nsor commented on Jan 10, 2024

    @t3nsor
    Author

    Although ?: can't be overloaded, it's definitely affected by this issue. Consider the original example if instead of having the - operator applied to s1 and s2, we had b ? s1 : s2 where b is some value of type bool. As cpplearner points out, [expr.cond]/6 does appeal to overload resolution using the built-in candidate signatures.

    For the = operator, I guess the wording needs some work. We could strike [over.match.oper]/5 and move the restriction into the proposed definition of "admissible".

  8. t3nsor commented on May 14, 2024

    @t3nsor
    Author

    OK, so here's what I was thinking of in my last comment:

    Merge [over.match.oper]/5 and [over.match.oper]/6 and make them into a note, with a cross-reference to [over.built].

    In the first paragraph of the previous proposed wording, make the following change:

    [...] If E has a class type and is not the left operand of the assignment operator, then T is also admissible for E if E's class has a non-template conversion function [...]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions