-
-
Notifications
You must be signed in to change notification settings - Fork 380
Description
The relevant spec section: Function Overloading
The goal of this issue is that the specification documents what the compiler does. From there, we can argue over what ought to be.
Non-template functions
The Spec
Function overloading occurs when two or more functions in the same scope have the same name. The function selected is the one that is the best match to the arguments. The matching levels are:
- No match
- Match with implicit conversions
- Match with qualifier conversion (if the argument type is qualifier-convertible to the parameter type)
- Exact match
IMO, the parenthesis is not needed. Obviously, if the qualifier conversion isn’t allowed, it’s a No match.
Named arguments are resolved for a candidate according to Matching Arguments to Parameters. If this fails (for example, because the overload does not have a parameter matching a named argument), the level is no match. Other than that, named arguments do not affect the matching level.
Each argument (including any
thisreference) is compared against the function's corresponding parameter to determine the match level for that argument. The match level for a function is the worst match level of each of its arguments.
Those two paragraphs are great.
Literals do not match
reforoutparameters.
Should be: “Rvalues do not match ref or out parameters, except that with -preview=rvaluerefparam, they do match ref parameters.”
However, this brings up a very relevant question: How do ref and value category (lvalue/rvalue) interact with matching levels? The spec says nothing about that, except that passing an rvalue to a ref (normally) is a no match. By what is said so far, the rules would say that lvalues match a ref parameter and a by-value parameter of the same type equally well.
scopeparameter storage class does not affect function overloading.
Prepend “The”, but otherwise clear. One might argue that scope should affect function overloading, but that’s not what the issue is about.
If two or more functions have the same match level, then partial ordering is used to disambiguate to find the best match. Partial ordering finds the most specialized function. If neither function is more specialized than the other, then it is an ambiguity error. Partial ordering is determined for functions f and g by taking the parameter types of f, constructing a list of arguments by taking the default values of those types, and attempting to match them against g. If it succeeds, then g is at least as specialized as f.
Remove “then”, all of them. Now, it sounds clear enough, but falls flat with ref. AFAICT, if a parameter is ref or out the synthesized argument is an lvalue and if it’s not, it’s an rvalue.
A function with a variadic argument is considered less specialized than a function without.
Is that checked before the partial ordering or only after partial ordering could not break the tie?
A static member function can be overloaded with a member function. The struct, class or union of the static member function is inferred from the type of the
thisargument.
Should be:
- “[…] with a non-static member function […]” ― a spec needs to be this precise
- “The struct, class, interface, or union of the static” because interfaces can have static member functions.
- “from the static type of the
thisargument” because it could, in principle, be the dynamic type for class or interface types
References
I’m not sure if the spec misses something or if, technically, everything is there, but it’s really under-explained. I base my next section on this example:
void f(ref immutable int) { } // 1
void f(immutable int) { } // 2
void f(ref const int) { } // 3
void f(const int) { } // 4a
void f(int) { } // 4b
immutable int x;
void main() => f(x);(They match in order as noted; 4a and 4b match equally well.)
As far as overload resolution is concerned, 1 and 2 are an exact match. Thus, partial ordering breaks the tie: Because 1 cannot be called with an rvalue (from 2), but 2 can be called with an lvalue (from 1), 1 is more specialized than 2. With -preview=rvaluerefparam, that is no longer the case, as an rvalue could be passed to 1, but the order is preserved (in particular, 1 is still preferred over 2). That means there must be some rule to prefer by-ref over by-value parameters for lvalues and by-value over by-ref parameters. Possibly, there must be two new levels added: One between 2 (Match with implicit conversions) and 3 (Match with qualifier conversion), i.e. 2½ (Match with qualifier and value category conversion), and one between 3 and 4, i.e. 3½ (Exact type match and value category conversion), where 4’s “exact match” would have to be clarified to mean “Exact type match and value category match”. Only that would explain why, with the preview switch enabled, 1 beats 2 and 3 beats 4a and 4b. Otherwise, 1 and 2 are both exact match, and all three of 3, 4a, and 4b are Match with qualifier conversion.
Function templates
Template functions are a whole other can of worms. The spec says literally nothing about overloading of templates: The Templates section mentions overloading not even once. Coming from C++, I know that function and function template overloading is a mess there and I expect it to be similar in D.
It’s not entirely clear whether discussions about function template overloading belongs to the Functions or Templates section. It fits the Functions section better IMO, but whoever tackles the issue can decide otherwise.
A function template is only considered if all non-template overloads are no match. Template parameter constraints (i.e. T : X, not if guards) play a role, and generally speaking, a function template that has the same number of template parameters and more constraints wins over a less constrained one:
void f(T1, T2)(T1, T2) { } // 1
void f(T1 : long, T2)(T1, T2) { } // 2
void f(T1, T2 : long)(T1, T2) { } // 3
void f(T1 : long, T2 : long)(T1, T2) { } // 4
void main() => f(1, 2); // chooses 4In this overload set, 4 wins over 2 and 3, and 2 and 3 win over 1, but 2 and 3 are equal, i.e. if 4 is removed, it’s an error unless 2 or 3 is also removed.
However, it’s not as simple as “overload resolution of function templates always prefers the more constrained template” (all else being similar/equal) because that would mean that a constrained function template would always win over one that’s not constrained at all – but that’s not the case:
void f(T1, T2)(T1, T2) { } // not constrained
void f(T1 : long)(T1, int) { } // constrained
void main() => f(1, 2); // ambiguous