Hacker News new | past | comments | ask | show | jobs | submit login
Overloading by Return Type in C++ (artificial-mind.net)
92 points by fanf2 9 months ago | hide | past | favorite | 51 comments



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.


The return-type overloading helps if you want to use non-static member initialization:

   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.


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.


> "look how clever I am" essays.

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.


Can always rely on C++ for hacks that are interesting, clever, and awful.

Apparently Haskell supports overloading on return type: https://stackoverflow.com/a/442291/


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.


Do you have any references about "omnidirectional type inference" in rust? A quick google doesn't bring anything obvious here.

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);


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.)

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.


An additional short-and-sweet example, using HashMap:

    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>`.


(you need foo.insert, but yes)


Er, you haven't heard about Rust's new feature where it treats all common metasyntactic variable names as interchangeable? :P


Ha! Well, I just realized I messed up left and right in my post so... we all do it.


Well, if it’s a trait that the type must implement (even if by the compiler), is it really overloaded? Because isn’t it technically

    fn Default<T>::default() -> T
? Which is polymorphism, not overloading? Idk. Maybe I’m just being pedantic?


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.


But rust doesn't even has function overloading


Rust "doesn't" have function overloading, but it effectively does:

  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...


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.


type classes, rust traits and overloading are all a form of ad-hoc polymorphism, hence the similarity.


They’re two very different features.


> Apparently Haskell supports overloading on return type

Ada does too.


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.

Found a good source eventually: https://learn.adacore.com/courses/Ada_For_The_CPP_Java_Devel...


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 :)

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.


It'd help with the edge case I showed... "safe" is a strong word though, doesn't handle this one:

  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.


I meant to write "safer" but the "r" fell through :)


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.


Please don't though.


We can put people on the moon, but we can't write a safe constructor in c++. I can only imagine 2050


There are probably more moving parts in a C++ compiler than the whole lunar landing program. :-)


We could put people on the moon. I doubt we are able to do it now in a 10 year time frame.


This is the reason that alternative general purpose languages are popular - just because you can write it in C++ doesn't mean you should :)


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”.


Yeah, agreed, there's a time and a place for this stuff. The art of programming is knowing when to break the rules, similar to most artistic pursuits.


What about good old return (void) and reinterpret_cast :)

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.


One of the nice things about Swift, is that we can do this. It was a pleasant surprise, when I found it out.

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...


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.


Hmm, this looks like the Scala implicit conversion trick


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.


Great article, but don't try this at home (or work).


I think this advice is incomplete

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


Yet another great feature that Perl did first :-)


TIHI


This kind of blog post is not the kind of blog post that gets you hired. It's the opposite... :)


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.




Applications are open for YC Winter 2022

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: