digitalmars.D.learn - implicit cast and overload priority
- Dom DiSc (59/59) Feb 13 Why does this happen:
- bkoie (6/11) Feb 13 trying to confused the compiler
- Paul Backus (13/27) Feb 14 Here's the spec for overload resolution:
- Andy Valencia (4/6) Feb 14 So I take it that a template with a static isSigned test would be
- Paul Backus (2/8) Feb 14 Yes, that's probably the easiest solution.
- Dom DiSc (9/15) Feb 15 Yes. But there could be another level (2b?) "match with implicit
- Paul Backus (8/13) Feb 15 The problem with this approach is that, if you keep adding more
- Jonathan M Davis (18/32) Feb 15 Honestly, I would argue that in most cases, if you're overloading on int...
- ShadoLight (29/31) Feb 16 I have often seen this mentioned, but I only see this happen if
- Jonathan M Davis (48/80) Feb 16 When I'm saying integer here, I mean integer types in general, not
- ShadoLight (7/7) Feb 17 On Sunday, 16 February 2025 at 17:04:39 UTC, Jonathan M Davis
- mig09 (3/11) Feb 17 If compilers could read minds, implicit matching would always be
- ShadoLight (3/16) Feb 17 True. And then compilers would also know that, of all the things
Why does this happen: ```d void foo(long x) { } void foo(ulong x) { } main() { foo(short(17)); // error: `foo` called with argument types `(short)` matches both: `foo(long x)` and: foo(ulong x)` } ``` Why does a short match an unsigned type with same priority as a signed type?!? Clearly a longer type with matching signedness should always be a better match than a type that will discard the sign, no? Because of this bug I often have to make weird workarounds like: ```d void foo(ulong x) { ... } void foo(T)(T y) if(isIntegral!T && isSigned!T) { long x = y; ... } ``` or vice versa. And another problem with overloads is, that only in the current module is searched for matches, even if functions with same name are imported explicitly: ```d module A; void foo(int x) { ... } module B; import A : foo; void foo(T)(T x) if(!is(Unqual!T==int)) { ... } main() { foo(-3); // error: template `foo` is not callable using argument types `!()(int)` Candidate is: `foo(T)(T x)` with `T = int` must satisfy the following constraint: `!is(Unqual!T == int)` } ``` A workaround in B is: ```d void foo(T)(T x) { static if(is(Unqual!T==int) return A.foo(x); // here explicitly a fully qualified name is required, else the compiler doesn't find it ... } ``` But this cannot be in A, because then again the error "matching both" occurs.
Feb 13
On Friday, 14 February 2025 at 04:28:25 UTC, Dom DiSc wrote:Why does this happen: ```d void foo(long x) { } void foo(ulong x) { } [...]trying to confused the compiler try that in c++ it will compile with no warnings how would the compiler actually know the dif between ulong 1, long 1, if not negative you prefixed some anotation ideally the short matches because it can be fitted or casted ideally
Feb 13
On Friday, 14 February 2025 at 04:28:25 UTC, Dom DiSc wrote:Why does this happen: ```d void foo(long x) { } void foo(ulong x) { } main() { foo(short(17)); // error: `foo` called with argument types `(short)` matches both: `foo(long x)` and: foo(ulong x)` } ``` Why does a short match an unsigned type with same priority as a signed type?!? Clearly a longer type with matching signedness should always be a better match than a type that will discard the sign, no?Here's the spec for overload resolution: https://dlang.org/spec/function.html#function-overloading In your example, both overloads have match level 2: "match with implicit conversions". So the compiler attempts to figure out which one is more specialized by checking whether the parameters of one overload can be used to call the other overload. * `long x` can be used to call `foo(ulong)`, so `foo(long)` is at least as specialized as `foo(ulong)`. * `ulong x` can be used to call `foo(long)`, so `foo(ulong)` is at least as specialized as `foo(long)`. Oops--it turns out both overloads are equally specialized! So you get an ambiguity error.
Feb 14
On Friday, 14 February 2025 at 15:58:26 UTC, Paul Backus wrote:So I take it that a template with a static isSigned test would be the way to bifurcate foo()'s behavior? Andyvoid foo(long x) { } void foo(ulong x) { }
Feb 14
On Friday, 14 February 2025 at 17:29:33 UTC, Andy Valencia wrote:On Friday, 14 February 2025 at 15:58:26 UTC, Paul Backus wrote:Yes, that's probably the easiest solution.So I take it that a template with a static isSigned test would be the way to bifurcate foo()'s behavior? Andyvoid foo(long x) { } void foo(ulong x) { }
Feb 14
On Friday, 14 February 2025 at 15:58:26 UTC, Paul Backus wrote:In your example, both overloads have match level 2: "match with implicit conversions". So the compiler attempts to figure out which one is more specialized by checking whether the parameters of one overload can be used to call the other overload.Yes. But there could be another level (2b?) "match with implicit conversion and signedness change", as new tiebreaker. This would be really easy to implement, and I can't see it causing any problems, as it would only allow to distinguish things that are today an error. So no code breakage.Oops--it turns out both overloads are equally specialized!This is what I want to be fixed. For a human it is obvious which of the two is a better match. Lets add a rule so that the compiler can also see it!
Feb 15
On Saturday, 15 February 2025 at 15:40:51 UTC, Dom DiSc wrote:On Friday, 14 February 2025 at 15:58:26 UTC, Paul Backus wrote:The problem with this approach is that, if you keep adding more and more rules, you eventually end up with so many that no human can remember them all, and code becomes much harder to understand. In C++, for example, overload resolution is extremely complicated, because they added a bunch of rules to make these kinds of "obvious" examples work: https://en.cppreference.com/w/cpp/language/overload_resolutionOops--it turns out both overloads are equally specialized!This is what I want to be fixed. For a human it is obvious which of the two is a better match. Lets add a rule so that the compiler can also see it!
Feb 15
On Saturday, February 15, 2025 1:12:43 PM MST Paul Backus via Digitalmars-d-learn wrote:On Saturday, 15 February 2025 at 15:40:51 UTC, Dom DiSc wrote:Honestly, I would argue that in most cases, if you're overloading on integer types, you're just asking for trouble - _especially_ when VRP comes into play, because then you get nonsense like foo(1) calling the bool overload. So, while I can understand being annoyed that the compiler doesn't choose the signed overload when you give it a signed number, I'd argue that it's far better for it to give an error about it than try to make it work, because the odds are just too high that you're going to end up with unexpected behavior if you're overloading on integer types to that fine a degree. Even with D's simplified overload resolution, you're still at a real risk of calling the wrong overload and ending up with subtle bugs. And in the cases where D _has_ tried to make the "obvious" stuff work - e.g. something like ubyte[] arr = [1, 2, 3]; it's resulted in all kinds of confusing inconsistencies in the language. So, while we learned from C++ with this stuff to some degree, we've still made plenty of mistakes with this sort of thing. - Jonathan M DavisOn Friday, 14 February 2025 at 15:58:26 UTC, Paul Backus wrote:The problem with this approach is that, if you keep adding more and more rules, you eventually end up with so many that no human can remember them all, and code becomes much harder to understand. In C++, for example, overload resolution is extremely complicated, because they added a bunch of rules to make these kinds of "obvious" examples work: https://en.cppreference.com/w/cpp/language/overload_resolutionOops--it turns out both overloads are equally specialized!This is what I want to be fixed. For a human it is obvious which of the two is a better match. Lets add a rule so that the compiler can also see it!
Feb 15
On Sunday, 16 February 2025 at 05:39:07 UTC, Jonathan M Davis wrote:_especially_ when VRP comes into play, because then you get nonsense like foo(1) calling the bool overload.I have often seen this mentioned, but I only see this happen if there *isn't* an integer overload i.e. this... ```d //void foo(int x) { writeln("I"); } //Comment to remove 'int' overload void foo(long x) { writeln("L"); } void foo(ulong x) { writeln("UL"); } void foo(bool x) { writeln("B"); } void main() { foo(17UL); foo(1); foo(true); } ``` ...will output: UL B B But if you uncomment the 'int' overload, you get... UL I B ...which is what I would expect. Or am I misunderstanding your point? With the 'int' overload commented, are you arguing it should match the best overloaded type that can VRP from 'int', such as 'long' above?
Feb 16
On Sunday, February 16, 2025 4:54:46 AM MST ShadoLight via Digitalmars-d-learn wrote:On Sunday, 16 February 2025 at 05:39:07 UTC, Jonathan M Davis wrote:When I'm saying integer here, I mean integer types in general, not necessarily int specifically. If you have an int overload specifically, then foo(1) will call the int overload, because that's an exact match given that integer literals are typed as int by default. However, it _is_ possible for foo(1) to match a bool overload if there isn't an int overload, because then implicit conversions come into play._especially_ when VRP comes into play, because then you get nonsense like foo(1) calling the bool overload.I have often seen this mentioned, but I only see this happen if there *isn't* an integer overload i.e. this...```d //void foo(int x) { writeln("I"); } //Comment to remove 'int' overload void foo(long x) { writeln("L"); } void foo(ulong x) { writeln("UL"); } void foo(bool x) { writeln("B"); } void main() { foo(17UL); foo(1); foo(true); } ``` ...will output: UL B B But if you uncomment the 'int' overload, you get... UL I B ...which is what I would expect. Or am I misunderstanding your point? With the 'int' overload commented, are you arguing it should match the best overloaded type that can VRP from 'int', such as 'long' above?The problem is two-fold: 1. It's actually possible for foo(1) to call a bool overload. This behavior is almost always surprising to folks and almost never what they actually want, because 1 is an integer value, not a boolean value, and almost everyone expects that passing an integer value to a function is going to call one of the integer overloads, not a bool overload. If this were C, it would make sense insofar as C doesn't have an actual bool type, but D does. A number of us tried to talk Walter out of this conversion behavior but unfortunately failed, basically because he's just too used to thinking like a C programmer. IMHO, integer literals should _never_ implicitly convert to bool, and in most languages, they don't. And for those who like to use integer values in conditional statements, those are different insomuch as they aren't actually implicit conversions. Rather, the compiler implicitly inserts an explicit cast to bool, which is why you can do stuff like if(ptr) instead of if(ptr !is null) even though pointers do not implicitly convert to bool. It's also why if you overload opCast!bool for a type, it will get used in the conditions for if statements, loops, and assertions. 2. Once VRP gets involved in general, the exact behavior you get can be surprising. Someone who really understands the rules and thinks it through can accurately determine which overload will be called, but to many programmers, the results tend to be surprising - especially if you don't sit down and carefully think through which overloads exist and how they interact with literals. It's also made worse by the fact that plenty of folks don't even know that VRP exists. VRP can at times be nice, because it reduces the need for explicit casts, but it tends to add to the confusion that can come with function overloads. And the fact that integer literals can be implicitly converted to bool just makes it worse. And honestly, I would think that it would be better to simply give an error if you pass any kind of integer literal to a function with multiple integer overloads rather than assuming the exact type of the literal, because it's _very_ easy to end up in a situation where you assume the type of the literal incorrectly (especially for less experienced D programmers). That's not how the language works, and I wouldn't expect it to change any more than integer literals will stop implicitly converting to bool, but I think that the current behavior in both cases is a mistake and error-prone. And there's just too much magic in general in D with regards to literals which tends to make certain stuff simpler at the cost of increasing complexity and confusion. - Jonathan M Davis
Feb 16
On Sunday, 16 February 2025 at 17:04:39 UTC, Jonathan M Davis wrote: [..]Ok, gotcha. I suspected that is what you meant and agree with that (in my example an implicit match would more sense on 'long', not 'bool'). Thanks for the detailed answer.
Feb 17
On Monday, 17 February 2025 at 09:17:24 UTC, ShadoLight wrote:On Sunday, 16 February 2025 at 17:04:39 UTC, Jonathan M Davis wrote: [..]If compilers could read minds, implicit matching would always be perfect)Ok, gotcha. I suspected that is what you meant and agree with that (in my example an implicit match would more sense on 'long', not 'bool'). Thanks for the detailed answer.
Feb 17
On Monday, 17 February 2025 at 09:33:32 UTC, mig09 wrote:On Monday, 17 February 2025 at 09:17:24 UTC, ShadoLight wrote:True. And then compilers would also know that, of all the things I've lost, I miss my mind the most!On Sunday, 16 February 2025 at 17:04:39 UTC, Jonathan M Davis wrote: [..]If compilers could read minds, implicit matching would always be perfect)Ok, gotcha. I suspected that is what you meant and agree with that (in my example an implicit match would more sense on 'long', not 'bool'). Thanks for the detailed answer.
Feb 17