Overloading by Return Type in C++
artificial-mind.net
artificial-mind.net
auto x = int(foo(blah));
is no shorter than the version with an explicit template parameter for the return type: auto x = foo<int>(blah);
That doesn't apply to situations where you can genuinely take advantage of the implicit conversion: takes_int_param(foo(blah));
But frankly the extra typing in an explicit version is worth it for improved code clarity: takes_int_param(foo<int>(blah));
That's just if you get it right - if you get something wrong and end up with a compiler error, the error message for the implicit conversion is going to be far more complex than for an explicit return type parameter.This would have been a good launching-off point to talk about the linker, or language design, which we did a bit of towards the end but in the wrong context, in my opinion. Instead this author was a bit too clever by half; a shorter article more focused on practical limitations might have been much more helpful to coders.
Yes. If you had general overload on return type, you could write big nested expressions and force the compiler to examine all the overload possibilities. With enough effort, you could write optimizers for functional programming that way, ones that figured out when you can create a pipeline and avoid storing an intermediate result. Plus combinatoric explosion in compile times. And then you would need more advanced algorithms to find a path through the type maze efficiently. What fun! Good for at least three PhD theses.
Don't go there. Someone will have to maintain that code.
struct Foo {
int x = init("x");
double y = init("y");
string z = init("z");
};
for not very good reasons you can't use auto there and having to specify the type twice is ugly.edit: this is pretty much the only case I have used this trick.
Apparently Haskell supports overloading on return type: https://stackoverflow.com/a/442291/
I am a bit confused here, are you talking about something that would require templates in C++?
template <T>
auto func(T t) { return t; }
y = func<int>(5);Here is the gist of it. Inferring types in C++ (and languages with type deduction) basically looks like this:
auto a = something;
Here, the type of a is determined by the type of something. The left hand side determines the type of the right hand side.In Rust (and languages with type inference), inferring types can look like that:
let a = something;
but it can also "go backwards": let a = 5;
let b: u64 = a;
Here, a will be a u64, because you later assign it to something of type u64, and so the compiler can "work backwards" to infer this. (In my understanding, it does not literally work backwards, but it feels like it.)To see how this plays out in your parent's comment, I adapted the code from: https://docs.microsoft.com/en-us/cpp/cpp/auto-cpp?view=vs-20...
#include<vector>
void func(std::vector<int> &vect)
{
vect.push_back(30);
}
int main()
{
std::vector<int> vect;
vect.push_back(10);
vect.push_back(20);
func(vect);
return 0;
}
You can't say "auto vect;" there, or else gcc will say "error: declaration of 'auto vect' has no initializer".But in Rust, you can write:
fn func(vect: &mut Vec<i32>) {
vect.push(30);
}
fn main() {
let mut vect = Vec::new();
vect.push(10);
vect.push(20);
func(&mut vect);
}
no type annotation on "let mut vect" there; it can see that you eventually pass it to func, and that's enough to infer the type. (For completeness, the line would be "let mut vect: Vec<i32> = Vec::new();" if you wrote out the type. use std::collections::HashMap;
fn main() {
let mut foo = HashMap::new();
foo.insert("bar", 42);
}
In C++, the declaration of `foo` would require a type annotation like `map<string_view, int>`. fn main() {
foo((42));
foo((12.0, 13.0));
}
pub fn foo(args: impl FooArgs) { args.exec() }
pub trait FooArgs { fn exec(self); }
impl FooArgs for u32 { fn exec(self) { println!("A number: {}", self) } }
impl FooArgs for (f32, f32) { fn exec(self) { println!("{} x {} = {}", self.0, self.1, self.0 * self.1) } }
https://play.rust-lang.org/?version=stable&mode=debug&editio... A number: 42
12 x 13 = 156
Usually you won't abuse tuples quite like this, but it's an option. The same pattern of using traits - without the tuples - is more common [1]. Or if you need variadic functions, typically you'd resort to a macro instead.[1]: https://medium.com/@jreem/advanced-rust-using-traits-for-arg...
fn Default<T>::default() -> T
? Which is polymorphism, not overloading? Idk. Maybe I’m just being pedantic?I don't think this is even significantly more code than a Haskell version would be, and the error messages and limitations are not significantly worse either.
Error messages for missing/ambigous overloads are necessarily nasty when there are many candidate overloads. And it is obvious that return-type overloading is not usable in situations where the desired return type is not known, as in the auto example.
As for the template error messages, since this is just one layer of templates, I suspect that they will be quite decent - especially considering that even one std::string-related error message is easily a few lines of almost completely irrelevant gibberish (does anyone actually use non-default char_traits???).
Ada does too.
Found a good source eventually: https://learn.adacore.com/courses/Ada_For_The_CPP_Java_Devel...
#include <string>
#include <string_view>
#include <iostream>
#include <sstream>
struct to_string_t {
std::string_view s;
// I'm too lazy to download boost::lexical_cast:
operator int() const { std::stringstream ss; ss << s; int r = 0; ss >> r; return r; }
operator bool() const { std::stringstream ss; ss << s; bool r = false; ss >> r; return r; }
};
to_string_t from_string(std::string_view s) { return to_string_t{s}; }
void takes_int(int i) {
std::cout << i << "\n";
}
int main() {
std::string foo = "1";
takes_int(from_string(foo + "0")); // OK
const auto a = from_string(foo + "0"); // temporary string
takes_int(a); // use after free bug, the temporary string was freed
}
MSVC's debug heap seems to have trouble catching it (small string optimizations?) and prints "10" followed by "0" (not another "10"!), but address sanitizer at least has my back: /mnt/c/local/evil$ g++ -fsanitize=address -std=c++17 main.cpp -o main && ./main
10
=================================================================
==154==ERROR: AddressSanitizer: stack-use-after-scope on address 0x7ffd0d828fb1 at pc 0x7f737d8cd733 bp 0x7ffd0d828ad0 sp 0x7ffd0d828278
...Something like this
template <class String>
struct to_string_t {
const String s;
...
}
template <class String>
auto from_string(String&& s) { return to_string_t<String>{std::forward<String>(s)}; }
Then the temporary's ownership would be passed to to_string_t. Would still have surprising behavior if a the argument is a non-const reference though. std::string json() { return "\"123\""; }
std::string_view unquote(std::string_view s) { return { s.data()+1, s.size()-2 }; }
auto i = from_string(unquote(json()));
It's a fundamental problem of subtly & unexpectedly deferred action.Overloading in C++ needs to be done sparingly so the compiler doesn't run into multiple possible conversions, which result in complex and confusing error messages.
A simple example is multiply-add. For your own types, you can have the compiler translate
a = b * c + d;
into a = multiply_add(b, c, d);
that means you can use familiar notation and yet use faster/more accurate composite functions.Stroustrup, in “the C++ Programming Language”, states he has seen a speed up of a factor of 30 using this ‘trick’ on matrix code.
He also advices against going over the top on it, though, so he agrees with “you can doesn’t imply you should”.
DISCLAIMER: Above is sarcasm.
I always had a beef with "turbo smart C++ tricks" like overloading ! operator to do something entirely different like send message to a process etc.
The only caveat, is that you need to specify the destination entity type (no implicit type). Not a big deal. Results in clearer code.
https://littlegreenviper.com/miscellany/swiftwater/swift_fun...
A string can contain anything and certainly not conform to its expected type. With a normal “error” return code (and overload-friendly type-specific parameter to find the converted value when successful), I can handle any type without magic and can also check errors. Futhermore, the return-parameter approach encourages me to actually think about errors instead of ignoring them (their function example of f(from_string(...), from_string(...)) is terrible).
> Thus, auto i = to_string_t{"7"}; does not work as intended.
It's my opinion that AAA is an unwise goal; uninformed use of 'auto' can lead to unintended copies. Sure, there are places where it's needed, an helps, but 'almost always'... not in my code base.
In general, I don't like to try risky new things in prod right away. Maybe I'll write some throwaway code or a test that uses the new thing. (This is how I started playing with ranges)
If it's an improvement, I'll leave it in, and as I figure out how to write clean code or fix the bugs with it, I'll start considering it in prod.
This is a useful trick! Also consider the alternative (eg in this case you can just write class functions on your type or use existing libraries)
If you get reasonable push back, don't use it. But I've seen it used in end seemed like sensible places in a production codebase and wouldn't say "never do this." The limits idea at the end seems fine, for example.
(The rust language ergonomics can be a good guide: try to balance "applicability", "power", and "context dependence". [1]
In the case of limits it is not very powerful, it's just a little syntax sugar. Seems ok.
I would say the from_string code has a few more footguns)
[1] https://blog.rust-lang.org/2017/03/02/lang-ergonomics.html
If we want to improve the hiring process, we should start applying a higher order degree of thinking when vetting candidates.