I think the contrast between string contains in Rust and C++ is illustrative. Overloading means you only get whatever parameter types the stdlib provides in C++.
I think the contrast between string contains in Rust and C++ is illustrative. Overloading means you only get whatever parameter types the stdlib provides in C++.
Overloading is a don't-repeat-yourself thing - if you have a bunch of functions that do fundamentally the same thing with different types, encoding those types in the function name when they're already explicit in the arguments is simply redundant.
Then there's an issue with ABI stability. Adding a new function argument breaks any existing compiled code even if there is a default value, but adding a new overload and redirecting the old one to call the new one with the defaults is fine.
That's where you should use parametric polymorphism, that's my point.
C++ 23 defines _3_ overloads of string.contains() with parameter defined as variously a char, a char * and a string view, enabling name.contains("Jim") name.contains("Steve"sv) and name.contains('Q'). But if you need a fourth, too bad.
Rust doesn't have overloading, so it defines string.contains() as polymorphic over the parameter type Pattern, which enables name.contains("Jim") name.contains('J') name.contains(char::is_uppercase) name.contains(['B', 'o', 'b']) name.contains(|c| { my_fun(c, 206, local_var) }) and so on and so forth.
One reason C++ doesn't do this is addressed in Circle, by offering "interfaces" which are approximately equivalent to Rust's traits or the C++ 0x Concepts (as Sean firmly points out, these were very different than Stroustrup's C++ 20 Concepts)
The problem is, what does 'Q' have in common with "Jim" ? In conventional C++ polymorphism we want a base class and of course these are basic types, they don't have a base class, much less one which has the necessary commonality.
With interfaces, we can say what we care about isn't some hypothetical "base class" of string_view and char, but instead a common interface they both have dedicated to string matching, and now we're writing polymorphic software again.
In C++ I need to overload doop() exactly three times, once for each of the three types we identified. I can't do it any other way, that's how it must be defined.
But in Rust I just write it once, in terms of Pattern, I don't need to understand how Pattern is actually implemented, that's opaque to me, I just use this Pattern trait and it all works.
edit: to elaborate: some functionality can be implemented generically (i.e. parametrically ) in term of some other concepts, recursively. At some point the concepts need to map to actual concrete implementation, then you use ad hoc polymorphism. This is the same in rust and in c++ [1].
Additionally in C++, even when you can implement some functionality parametrically, it is sometime useful to (partially) specialize functions and types to take advantage of some optimizations (for example it is possible to implement std::copy generically, but it is often specialized for contiguous iterators to trivially copyable types to call memcpy).
[1] Rust does of course have a more principled handling of these concepts, which are often underdefined in c++.
Do you have an example I can look at where it's done the way you imagine it "should" be done in C++?
The reason the standard specifies the three overloads of course is because chars and chars literals that have been inherited from C are funny and those begin/end pairs are dangerous. But that's a quirk of C++ string types and nothing to do with parametric and ad-hoc polymorphism in C++.
edit: also the three overloads in MSVC probably forward to the same implementation over string_view (except possibly for char of course that can be implemented more efficiently).
edit2: I use concepts in my implementation just because. It would work just fine without them.
I believe that your hypothetical approach, ignoring the fact it's not practical, is parametric polymorphism. We can invent more types of "needle" and use the same "needle" concept for other predicates, thus if we have N needles and M predicates that's N+M work, not N*M work as with the overloads.
However, what is actually practical, and thus what is done, is just ad hoc polymorphism via the overloading we looked at, if WG21 wants to expand it that's N*M implementation cost.
I do not believe we can claim something is ad hoc polymorphism solely because somewhere there's different implementation code for type A and type B as with your Godbolt example, if you believe that's ad hoc polymorphism surely you end up thinking std::sort is also ad hoc polymorphism which seems like nonsense.
My "hypothetical" approach is bog-standard generic programming in c++ has it has been practiced for almost 30 years since Stepanov originally codified it.
ADL was litterally designed to make this sort of stuff work.
You're right that in some places C++ does the generic thing, I pointed at std::sort already - there are lots of examples of varying quality. The way we got here is that I pointed out overloads are the Wrong Thing™ and that's why it makes sense Carbon would want to outlaw them, and why no, outlawing them does not lose the nice property where the API feels generic - you can do parametric polymorphism and still have the generic API.
The main thing outlawing overloading gets rid of is actually functions/ methods which quietly have more than one distinct purpose. These functions are foot guns. Use your words, call the two (or more) things by different names reflecting the difference in intent and then fewer developers will mistakenly invoke the wrong meaning.
I don't like ADL either, but it's pretty far off topic.