As many on the /r/cpp thread for this said, the ultimate result:
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.
Yeah, this is something that might have made sense before auto, and in fact IIRC there were some Boost libraries that used this trick (Boost.Assign?), but it's just unnecessary complication at this point.
Yes. I agree. I thought the essay might be useful or informative, but about a third of the way through I realized that this was one of these "look how clever I am" essays.
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.
I would say that, especially by C++ standards, this is surprisingly simple, easy to understand, and fully featured - even the extensible template-based version.
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???).
Rust also supports it, and it's actually very commonly used. For example, the function `Default::default()`, where Default is a trait that you can easily implement for your own types, too.
I always thought unidirectional type inference (i.e. C++ 'auto') was good enough but I have been surprised to find omnidirectional type inference very useful in Rust... for example, constructing an empty collection to pass as the parameter to a function.
Some people describe this split as "type deduction" vs "type inference," that is, C++ has deduction, Rust has inference.
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.)
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.
Rust has return type polymorphism rather than return type "overloading", but the difference only manifests for library authors; for API consumers the interface is the same either way.
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.
It does, in a sense; you could say it has a “structured” way to overload functions that significantly simplifies resolution and predictability. People don’t really call it overloading, but it has many similarities.
I was going to include Ada in my comment, I was sure I'd read that it supports overloading on return type, but I couldn't find a supporting source with a quick google.
to_string_t as shown is bait for undefined behavior, when combined with auto:
#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
...
It could be made safe by making to_string_t a template and forwarding from_string's argument into its storage, if things are not complicated enough already :)
While this is a clever hack, it's brittle (as the article points out), with too many cases where it doesn't work right. It's just not going to be maintainable.
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.
This is explicitly supported in Rust (with generics). The most well known example is the `.collect()` method, which can collect an iterator into many different types of collections.
But it also doesn’t mean you should not. Sometimes, the technique of returning a promise (“here’s something that can be converted to what you asked for, even though you didn’t tell yet exactly what you want”) is very useful.
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”.
This is not a great example because a string conversion by nature requires error-checking and should not be a simple return-by-value.
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).
This is quite interesting. I'm going to experiment with this in our code that returns query results from SQLite and Redis. If it works it could simplify a lot of code.
> 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 some circumstances you can use templates to approximate this with less surprising edge cases in exchange for not being as transparent in the common case.
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)
Depends, sort of. The code shows some creativity and shows you know how to work with templates. Sure, it's 2020, but I'm pretty sure there's quite a lot of people who have used C++ but cannot come up with the code you see in the final version. Which shouldn't be a problem I guess, but only as long as you're just an API consumer and don't have to produce one.
Why? This piece shows that the author seems to be resourceful, creative and has a reasonably good understanding of C++. Would a change like this land in production? That's probably what code reviews are for.
If we want to improve the hiring process, we should start applying a higher order degree of thinking when vetting candidates.
It would be enough to get an interview for me. :) They would have to tell me when they felt it was appropriate and inappropriate to use such a trick to get a hire. It feels like most C++ devs don't know SFINAE - I'd rather find someone who knows deeper bits of the language and encourage them to use their talents appropriately.