cplusplus / CWG

Core Working Group
24 stars 7 forks source link

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

Open t3nsor opened 1 year ago

t3nsor commented 1 year ago

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
  • [...]
t3nsor commented 1 year ago

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.

t3nsor commented 1 year ago

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);

t3nsor commented 10 months ago

bump

jensmaurer commented 9 months ago

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.

cpplearner commented 9 months ago

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.

t3nsor commented 9 months ago

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".

t3nsor commented 5 months ago

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 [...]