cplusplus / CWG

Core Working Group
23 stars 7 forks source link

CWG2730 [over.match.oper] Unified overloading of relational operators #318

Open tobias-loew opened 1 year ago

tobias-loew commented 1 year ago

Full name of submitter (unless configured in github; will be published with the issue): Tobias Loew

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

Link to reflector thread (if any):

Issue description:

The standard allows to overload operators when at least one of its arguments is (a reference to) a possible cv-qualified class or enumeration type. To prevent ambiguities during overload resolution, a user-defined operator hides a built-in operator when both have the same parameter-type-list - but there exists the restriction, that only non-template user-defined operators do so. The rationale behind this is to prevent template functions like

template unspecified-type operator @(T,T); // (1)

or

template<typename T, typename S> unspecified-type operator @(T,S); // (2)

from hiding built-in operators, as this would introduce overloading operators for built-in types through the backdoor.

Of course, templated operators like (1) or (2) get called when at least one of the parameter types is (a reference to) a class, or (a reference to) an enumeration. But the standard introduces here an exception: A user-defined templated operator is NOT called when:

The reason for this exception is that [over.built] (15) defines all relational operators for T, and as the user-defined operator is a template, the built-in relational operator is not rejected (cf. [over.match.oper] (3.3.4)). Thus, overload resolution selects the non-template built-in operator over the templated user-defined.

This leads to the situation that user-defined operators of the form (1) or (2) are selected over built-in operators, except when:

This rather unintuitive behavior could be fixed, when clause [over.match.oper] (3.3.4) was replaced by a combination of it with [over.oper] (7).

Suggested resolution: Proposed change of wording:

[over.match.oper] (3.3.4):

That way, all templated operators would be handled like non-template operator overloads, but overloading of operators for built-in types was still prohibited.

Of course, this would be a breaking change for overloading relational operators for enumerations, but a quick search for templated overloads of operator < of the form "template bool operator <" did not show any match in the codebases of LLVM or Boost (1.75). Even more, as this exception is rather unknown, if there was a match in an existing codebase, there are good chances that the intuition was to overload the built-in operator.

One application of the proposed change, is using type-traits to prevent usage of the operators <, <=, > and >= for enumerations used as flags (cf. https://github.com/tobias-loew/bitwise_operators).

jensmaurer commented 1 year ago

Could you please show a simple five-line (or so) example, contrasting the behavior in the status quo with the desired behavior? Are only relational operators affected, or also equality operators?

In your suggested resolution, why can't we simply strike "that is not a function template specialization"? This section is defining built-in candidates, and non-member candidates are all user-defined, but they are already constrained by [over.oper] p7, so it doesn't seem necessary to repeat that restriction here.

Please see CWG2673 for a superficially related issue in that area.

For your specific problem of preventing the use of certain built-in operators: why aren't you using a strongly-typed enumeration ("enum class")?

tobias-loew commented 1 year ago

Could you please show a simple five-line (or so) example, contrasting the behavior in the status quo with the desired behavior? Are only relational operators affected, or also equality operators?

Affected are all comparison opererators, <, <=, >, >=, ==, != and <=> as specified in [over.built]#15 Currently, the standard allows overloading those operators on an enumeration only with a function (not a function-template)

For

enum class flags {
    a = 0x1,
    b = 0x2,
};

the following works

// prevent user from doing < on enum `flags`
bool operator < (flags, flags) = delete;
bool b = flags::a < flags::b;               // intended compilation error

but the following works doesn't work

template<typename E>
struct disable_order_operators : std::false_type {};

// prevent user from doing < on enum that fullfils type-trait `disable_order_operators`
template<typename E>
std::enable_if_t<disable_order_operators<E>::value, bool> operator < (E, E) = delete;

template<>
struct disable_order_operators<flags> : std::true_type {};

bool b = flags::a < flags::b;               // DOES NOT give the intended compilation error
//                ^ calls builtin operator <(flags, flags) instead

In your suggested resolution, why can't we simply strike "that is not a function template specialization"? This section is defining built-in candidates, and non-member candidates are all user-defined, but they are already constrained by [over.oper] p7, so it doesn't seem necessary to repeat that restriction here.

I'm not a language lawyer, but yes, I think your right.

template<typename T1, typename T2>
bool operator < (T1, T2);

would be a better match when comparing different built-in types, but it's prohibited by [over.oper]p7

Please see CWG2673 for a superficially related issue in that area. I'll ask Barry. He's currently also at CppNow

For your specific problem of preventing the use of certain built-in operators: why aren't you using a strongly-typed enumeration ("enum class")?

The problem exists for both scoped and unscoped enumerations: the referenced library provides a non-intrusive way to turn an enum of bit-flags into a mathematical sound boolean algebra (without any runtime overhead). For scoped enums, the use of the bit-operations &, |, ^ etc. are disable by default (which IMHO is a good thing), but I explicitly want to disable (or override) the order-comparison operator: the usage of flags should abstract away the underlying value used for representation, furthermore the mathematically correct interpretation of the order-comparison is set-inclusion of flags and not comparison of the underlying value.

brevzin commented 1 year ago

Same example, slightly less typing:

enum class flags {
    a = 0x1,
    b = 0x2,
};

template<typename E>
inline constexpr bool disable_order_operators = false;

// prevent user from doing < on enum that fullfils type-trait `disable_order_operators`
template <typename E> requires disable_order_operators<E>
void operator < (E, E) = delete;

template <>
inline constexpr bool disable_order_operators<flags> = true;

bool b = flags::a < flags::b; // ok: should be ill-formed

And yeah, we talked about the wording, and you're right Jens, just removing that clause sounds right to us:

do not have the same parameter-type-list as any non-member candidate or rewritten non-member candidate that is not a function template specialization.

jensmaurer commented 1 year ago

CWG2730