Inconsistent Operator Overload Resolution When Using Operators with "Function Style" Syntax ('(Binaryop)(Lhs, Rhs)', and '(Unaryop)(Operand)')

inconsistent rules for operator overloading in swift

The difference is that the standard library currently has a + overload defined directly on Float:

//  TODO: These should not be necessary, since they're already provided by
// <T: FloatingPoint>, but in practice they are currently needed to
// disambiguate overloads. We should find a way to remove them, either by
// tweaking the overload resolution rules, or by removing the other
// definitions in the standard lib, or both.

extension ${Self} {
@_inlineable // FIXME(sil-serialize-all)
@_transparent
public static func + (lhs: ${Self}, rhs: ${Self}) -> ${Self} {
var lhs = lhs
lhs += rhs
return lhs
}

// [...]
}

(where ${Self} will be substituted by Float, Double & Float80 upon gyb.py running)

When it comes to overload resolution, a (Float, Float) -> Float overload of + will win over a (Value, Value) -> Float overload when applied with Float operands, as the latter will require them to be converted from Float to Value.

However Float doesn't have a > overload defined directly on it; it's instead defined as a top-level generic function over FloatingPoint operands:

@_transparent
public func > <T : FloatingPoint>(lhs: T, rhs: T) -> Bool {
return rhs.isLess(than: lhs)
}

When it comes to overload resolution, your (Value, Value) -> Bool overload will win over this generic overload as overload resolution favours non-generic overloads (see the tangent in my answer here). Therefore you'll wind up recurring. You could fix this by making your overload of > generic over <T : Value>(T, T) -> Bool, but then it would no longer be applicable to heterogenous Value operands.

Assuming you want the operators to be applicable to heterogenous operands, one foolproof solution would be to dispatch the calls to > and + via Float's conformance to Numeric and Comparable by using nested generic functions. This can't recurse because neither
(Value, Value) -> Float nor (Value, Value) -> Bool can satisfy the + and > requirements of these respective protocols.

func + (lhs: Value, rhs: Value) -> Float {
func add<T : Numeric>(_ lhs: T, _ rhs: T) -> T {
return lhs + rhs
}
return add(lhs.get(), rhs.get())
}

func > (lhs: Value, rhs: Value) -> Bool {
func greaterThan<T : Comparable>(_ lhs: T, _ rhs: T) -> Bool {
return lhs > rhs
}
return greaterThan(lhs.get(), rhs.get())
}

(Note we're doing this for + as well as >, as the + (Float, Float) -> Float overload may be removed in favour of a generic overload, which would cause the same problems as with >)

Another solution would be to avoid dispatching to > and + altogether, and instead just call the isLess(than:) method on Float and the += operator, mimicking the stdlib implementations:

func + (lhs: Value, rhs: Value) -> Float {
var lhsFloat = lhs.get()
lhsFloat += rhs.get()
return lhsFloat
}

func > (lhs: Value, rhs: Value) -> Bool {
return rhs.get().isLess(than: lhs.get())
}

Though the calling of += may be problematic if you implement an overload of it for (inout Value, Value) -> Void.

Confused about operator overloading

Why is it called overloading?

It's called "overloading" because it is overloading. We "overload" a thing when we give two possible implementations for that thing and then must decide which to use (which is called overload resolution).

When we overload methods we give two or more implementations of a method with a given name. When we overload operators we give two or more possible implementations for an operator with a given syntax. It's the same thing.

Make sure you're not confusing overloading with overriding. Overloading is simply the existence of two methods/operators with the same name/syntax in the same declaration space. Overriding deals with how the content of a virtual method slot is filled in at runtime.

Do all objects by default have an implementation of all operators?

No.

Is public static object operator +(object o1, object o2) predefined somewhere and somehow?

No.

This would somehow imply that by default objects do not have predefined operators so then how come the term overloading?

I don't understand the question. Of course C# has predefined operators.

I don't want to expose this operator publicly

Then don't use an operator; make a private, internal or protected method.

The design of C# is that an operator is always part of the public surface area of a type. It is very confusing to have operators that have a meaning that depends on what accessibility domain the usage occurs in. C# has been carefully designed to be a "pit of quality" language, where the choices of the language designers lead you away from writing confusing, buggy, hard-to-refactor programs. Requiring that user-defined operators be public and static is one of those subtle design points.

(1) Objects by default do not have predefined operators

Sure they do; there are hundreds of predefined operators on a variety of objects. For addition, for example, there are the following predefined overloads of operator +:

int + int
uint + uint
long + long
ulong + ulong
double + double
float + float
decimal + decimal
enum + underlying (for any enum type)
underlying + enum
int? + int?
uint? + uint?
long? + long?
ulong? + ulong?
double? + double?
float? + float?
decimal? + decimal?
enum? + underlying?
underlying? + enum?
string + string
object + string
string + object
delegate + delegate (for any delegate type)

Consult the C# specification for a list of all the predefined overloads of all the other operators.

Note that overload resolution for operators has two phases: first, overload resolution attempts to find a user-defined overload that is the unique best; only if doing so finds no applicable candidates are the predefined overloads considered by overload resolution.

(2) Defining operators is not described as creating an operator, it's described as overloading which somehow is inconsistent (to me) with point 1.

I don't understand why you find it inconsistent, or, for that matter, what you find inconsistent. The term "overload" is used consistently to describe both operators and methods; in both cases it means to use the same syntax to refer to two or more different things, and that ambiguity is then resolved via "overload resolution". The exact details of the method and operator overload resolution algorithms are different but they are similar in the overall algorithm: first a candidate set is identified, then inapplicable candidates are removed, then a betterness algorithm eliminates applicable candidates that are worse than another, then a bestness algorithm determines the unique best candidate that is left, if any.

(3) You cannot restrict access modifier of an operator, they have to be public, which does not make sense considering point 1. but sort of makes sense considering point 2.

I don't understand what point (3) has to do with points (1) or (2) at all. The restriction that operators must be part of the public surface area is to prevent the confusing situation of being able to add a Fruit to an Animal when you're inside class Apple but not when you're inside class Giraffe.

Operators are declared inside a class or struct and therefore "belong" to said type, they don't float "belonging" to no given type. So what am I overloading exactly when I declare an operator in a class?

You're overloading the operator.

That the same operator exists between ints does not mean I am overloading anything as that operator belongs to int. To me its the same as saying the Foo.Hello() and Bar.Hello(string hello) are overloads of Hello. They are not as they are declared in two separate types. What is the difference with operators?

You've just accurately described the difference. Method overloading and operating overloading differ in many of their details.

If you want to take the position that Foo.Hello() and Bar.Hello(string) are "overloads" of Hello, that's not a common position to take but it is logically consistent.

I was under the impression that you cannot change the access modifier when overloading.

Your impression is mistaken; you cannot change access modifiers when overriding a virtual method. You've confused that with overloading.

(And I note that there is one scenario in which you are required to change the access modifier when overriding a virtual method; can you deduce what it is?)

I was also under the impression that you can not declare an operator without at least one of the operands being of the type in which you declare the operator.

That's almost correct. A user-defined operator must have an operand of type T, where T is the enclosing class or struct type, or T? if T is a struct type.

So how can a third class have access to a given operator while another third class can't unless one belongs to an external assembly and the other doesn`t in which case I do not find it confusing at all and even useful?

You've mischaracterized my example, which could have been more clear. This is illegal:

public class Fruit 
{
protected static Shape operator +(Fruit f, Animal a) { ... }
}

Because this is bizarre:

public class Apple : Fruit
{
...
Shape shape = this + giraffe; // Legal!
}
public class Giraffe : Animal
{
...
Shape shape = apple + this; // Illegal!
}

This is just one example. In general it is a strange thing to do, to make overload resolution of an operator depend on the accessibility domain, so the language designers ensured that this never happens by requiring user-defined operators to be public.

I simply find overloading confusing in the context of operators.

Many people do, including compiler writers. The user-defined operators portion of the specification is extremely difficult to parse, and the Microsoft implementation is a rich source of compiler bugs, many of which were my fault.

I don't see why simply declaring operators in a type has to described differently from how you would describe declaring any other static method.

Well, different things are different; operators are different than methods in many ways, including their overload resolution algorithms.

I have never particularly liked that C# has overloadable operators. The C# feature is a somewhat better designed version of the same feature in C++, but in both languages the feature in my opinion entails far larger costs than the corresponding user benefits.

Thank goodness at least C# does not thoroughly abuse the << operator the way idiomatic C++ does -- though of course it does abuse + and - for delegates.

Overload resolution for multiply inherited operator()

Barry got #1 right. Your #2 hit a corner case: captureless nongeneric lambdas have an implicit conversion to function pointer, which got used in the mismatch case. That is, given

struct foo : foo_int, foo_str {
using foo_int::operator();
//using foo_str::operator();
foo(): foo_int(print_int), foo_str(print_str) {}
} f;

using fptr_str = void(*)(const char*);

f("hello") is equivalent to f.operator fptr_str()("hello"), converting the foo to an pointer-to-function and calling that. If you compile at -O0 you can actually see the call to the conversion function in the assembly before it gets optimized away. Put an init-capture in print_str, and you'll see an error since the implicit conversion goes away.

For more, see [over.call.object].

Resolution of built-in operator == overloads

This is CWG 507. An example similar to yours was given, and the submitter explained that according to the standard, the overload resolution is ambiguous, even though this result is very counter-intuitive.

Translating to your particular example, when comparing operator==(int, int) and operator==(float, int) to determine which is the better candidate, we have to determine which one has the better implicit conversion sequence for the first argument (obviously in the second argument, no conversion is required). For the first argument of operator==(int, int), we just use A::operator int. For the first argument of operator==(float, int), there is no way to decide whether to use A::operator int or A::operator char, so we get the "ambiguous conversion sequence". The overload resolution rules say that the ambiguous conversion sequence is no better or worse than any other user-defined conversion sequence. Therefore, the straightforward conversion from A{} to int (via A::operator int) is not considered better than the ambiguous conversion from A{} to float. This means neither operator== candidate is better than the other.

Clang is apparently following the letter of the standard whereas GCC and MSVC are probably doing something else because of the standard seeming to be broken here. "Which compiler is right" depends on your opinion about what the standard should say. There is no proposed resolution on the issues page.

I would suggest removing operator char unless you really, really need it, in which case you will have to think about what else you're willing to give up.

Should this compile? Overload resolution and implicit conversions

It should be easier to picture why the overload resolution is ambiguous by going through it step-by-step.

§13.5.5 [over.sub]

Thus, a subscripting expression x[y] is interpreted as x.operator[](y) for a class object x of type T if T::operator[](T1) exists and if the operator is selected as the best match function by the overload resolution mechanism (13.3.3).

Now, we first need an overload set. That's constructed according to §13.3.1 and contains member aswell as non-member functions. See this answer of mine for a more detailed explanation.

§13.3.1 [over.match.funcs]

p2 The set of candidate functions can contain both member and non-member functions to be resolved against the same argument list. So that argument and parameter lists are comparable within this heterogeneous set, a member function is considered to have an extra parameter, called the implicit object parameter, which represents the object for which the member function has been called. [...]

p3 Similarly, when appropriate, the context can construct an argument list that contains an implied object argument to denote the object to be operated on.

// abstract overload set (return types omitted since irrelevant)
f1(Something&, foo const&); // linked to Something::operator[](foo const&)
f2(std::ptrdiff_t, char const*); // linked to operator[](std::ptrdiff_t, char const*)
f3(char const*, std::ptrdiff_t); // linked to operator[](char const*, std::ptrdiff_t)

Then, an argument list is constructed:

// abstract argument list
(Something&, char const[3]) // 'Something&' is the implied object argument

And then the argument list is tested against every member of the overload set:

f1 -> identity match on argument 1, conversion required for argument 2
f2 -> conversion required for argument 1, conversion required for argument 2 (decay)
f3 -> argument 1 incompatible, argument 2 incompatible, discarded

Then, since we found out that there are implicit conversions required, we take a look at §13.3.3 [over.match.best] p1:

Define ICSi(F) as follows:

  • if F is a static member function, [...]; otherwise,
  • let ICSi(F) denote the implicit conversion sequence that converts the i-th argument in the list to the type of the i-th parameter of viable function F. 13.3.3.1 defines the implicit conversion sequences and 13.3.3.2 defines what it means for one implicit conversion sequence to be a better conversion sequence or worse conversion sequence than another.

Now let's construct those implicit conversion sequences for f1 and f2 in the overload set (§13.3.3.1):

ICS1(f1): 'Something&' -> 'Someting&', standard conversion sequence
ICS2(f1): 'char const[3]' -> 'foo const&', user-defined conversion sequence
ICS1(f2): 'Something&' -> 'std::ptrdiff_t', user-defined conversion sequence
ICS2(f2): 'char const[3]' -> 'char const*', standard conversion sequence

§13.3.3.2 [over.ics.rank] p2

a standard conversion sequence (13.3.3.1.1) is a better conversion sequence than a user-defined conversion sequence or an ellipsis conversion sequence.

So ICS1(f1) is better than ICS1(f2) and ICS2(f1) is worse than ICS2(f2).

Conversely, ICS1(f2) is worse than ICS1(f1) and ICS2(f2) is better than ICS2(f1).

§13.3.3 [over.match.best]

p1 (cont.) Given these definitions, a viable function F1 is defined to be a better function than another viable function F2 if for all arguments i, ICSi(F1) is not a worse conversion sequence than ICSi(F2), and then [...]

p2 If there is exactly one viable function that is a better function than all other viable functions, then it is the one selected by overload resolution; otherwise the call is ill-formed.

Well, f*ck. :) As such, Clang is correct in rejecting that code.

Are free operator-* overloads evil?

Googling around a bit, I found more instances of people asking whether operator->* is ever used than actual suggestions.

A couple places suggest T &A::operator->*( T B::* ). Not sure whether this reflects designer's intent or a misimpression that T &A::operator->*( T A::* ) is a builtin. Not really related to my question, but gives an idea of the depth I found in online discussion & literature.

There was a mention of "D&E 11.5.4" which I suppose is Design and Evolution of C++. Perhaps that contains a hint. Otherwise, I'm just gonna conclude it's a bit of useless ugliness that was overlooked by standardization, and most everyone else too.

Edit See below for a paste of the D&E quote.

To put this quantitatively, ->* is the tightest binding operator that can be overloaded by a free function. All the postfix-expression and unary operators overloads require nonstatic member function signatures. Next precedence after unary operators are C-style casts, which could be said to correspond to conversion functions (operator type()), which also cannot be free functions. Then comes ->*, then multiplication. ->* could have been like [] or like %, they could have gone either way, and they chose the path of EEEEEEVIL.



Related Topics



Leave a reply



Submit