C++ Logo

std-discussion

Advanced search

Overloading relational operators for enumerations with templates

From: tobi_at <tobi_at_[hidden]>
Date: Sun, 03 Jan 2021 18:53:16 +0100
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 here exists the restriction, that only
non-template user-defined operators do so.
The rationale behind that seems to be preventing template functions like
template<class T> unspecified-type operator @(T,T); // (1)
from hiding built-in operators, as this would introduce overloading
operators for built-in types through the backdoor.

Nevertheless, templated operators like (1) are called when at least one
of the parameter types is a class, a reference to a class, an
enumeration, or a reference to an enumeration, with one exception: a
user-defined templated operator is NOT called when T is an enumeration
type and @ is a relational operator.
(Yet for any concrete enumeration E1 all relational operators can be
overloaded by non-template operators.)

The reason is that [over.built] (18) 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.

Please also note, that overloading operators for unscoped enumerations
with template functions works for all following operators as the
built-in versions invoke conversions to the underlying type (scoped
enumerations do not allow those built-in operators):
+, -, *, /, %, ^, &, |, ~, !, +=, -=, *=, /=, %=, ^=, &=, |=, <<, >>,
>>=, <<=, ++, --, && and || (I hope the list is complete)

This leads to the funny situation that a user-defined operator is
selected over a built-in operators except when
- the operator is a relational operator,
and
- the parameter-type-list is (E,E) where E is a (cv- qualified
reference) to an enumeration
and
- the user-defined operator is a template

This unpleasant and rather unintuitive behavior could be fixed, when
clause [over.match.oper] (3.3.4) was replaced by a combination of it
with [oper.over] (7):
- do not have the same parameter-type-list as any non-member candidate
that has at least one parameter whose type is a class, a reference to a
class, an enumeration, or a reference to an enumeration.

That way, all templated operators would also become "first class"
operator overloads, but the 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<class T> bool operator <" did not
show any match in the codebases of LLVM or Boost (1.75). Even more, if
there was a match in an existing codebase, there are good chances that
it was meant to overload the built-in operator.

I stumbled over that "feature" when I implemented a small library to
non-intrusively support bitwise-operations on flag-like enumerations:
https://github.com/tobias-loew/bitwise_operators
Since for many enumerations and especially for flag-like enumerations
the effective value of the enumerators should not determine the
semantics of the program, I also wanted to raise a compilation error
when one of the operators <, >, <= or => was used. But it turned out to
be currently impossible using template-based overloads for those
operators.

Received on 2021-01-03 11:53:27