Replacing C++ overloading with idomatic Rust

Hello!

I love Rust and if I don't have to write any more C++ code, I would be very happy. One challenge I frequently face is how to use Rust generics to solve problems, for which using C++ I would use an overloaded function to achieve.

This stack overflow article was written by someone like me in this regard, but sadly it is out of date and the original solution the author provides does not compile:

stack overflow article

The question amounts to, how does one create, for example an add function in Rust that, when given two i32 args, yeilds an i32 result, and when given two f32 args yields an f32 result.

Referring to the stackoverflow article, it would seem that at the time it was written the following code would have worked. (I do not know if this is true. For example I had to fix the error that results due to the incorrect literal in the example (10i instead of 10i32)). I'm assuming there was a day when simply 10i would have worked as well. Regardless the version below still does not compile:

fn add<T: Add<T,T>>(a: T, b: T) -> T{
    a + b
}
fn main() {

    println!("{}", add(10i32,  5i32));
}
 

And this is the error generated:

error[E0405]: cannot find trait `Add` in this scope
 --> src/main.rs:1:11
  |
1 | fn add<T: Add<T,T>>(a: T, b: T) -> T{
  |           ^^^ not found in this scope
  |
help: consider importing one of these items
  |
1 | use core::ops::Add;
  |
1 | use std::ops::Add;
  |

Any discussion here on how to fix the code here, or further how to achieve similar results to migrate from C++ overloading, to advisable idiomatic rust code, altogether would be greatly appreciated!

Link is not clickable - did you miss the address?

Here, the compiler already suggests a fix - you can add use std::ops::Add; to the top of the file, or use the full name of the trait as in fn add<T: std::ops::Add<T, Output = T>> (note that this is the correct syntax, since the trait has one parameter and one associated type, not two parameters).

In more general case, "overloading" is indeed usually done with traits - instead of making fn foo(arg: T1) and fn foo(arg: T2), you make trait Fooable and fn foo<T: Fooable>(arg: T). The problem might arise with overloading over arity; in this case it'd probably be idiomatic to collapse all arguments into one structure (or tuple).

4 Likes

I strongly suspect the example you're working from was intended to include an additional Add trait. The built-in one (which the compiler recommended to you) only has a single type argument, and has some subtleties about the result type. However, using it does let your code compile:

use std::ops::Add;

fn add<T: Add<T>>(a: T, b: T) -> T::Output {
    a + b
}

fn main() {
    println!("{}", add(10i32,  5i32));
}

It's not clear what you mean by "overloading" here. In C++, "overloading" is generally the practice of having multiple functions with the same name, using differences in signature to determine which function to invoke. Your StackOverflow link isn't working for me, unfortunately, so I can't see if there's more context, but if that's the gist of it, then in Rust the normal way to do that is to give the functions different names.

If you mean something else, would you be willing to say more?

1 Like

Thank you for the quick reply. I will review and try your solution.

Sorry about that I just recreated the link. Hopefully it now works.

Thank you for your reply. I tried to correct the link. Sorry about that. Please let me know if it still does not work.

To answer your question:

You are correct that "overloading" refers to the practice of using common function names with different signatures; and so the basic idea is that, for example, nothing prevents one from creating two add functions (global functions or as members of a class) to accomplish the goal described.

So in C++ for example you could have:

int add(int x, int y) { return x + y; }
double add(double x, double y) { return x + y; }

void main() {
        double dx = 3.7;
        double dy = 10.4; 
        printf("double result  = %f", add( dx, dy));

        int ix = 10;
        int iy = 23;
        printf("int result = %d\n", add(ix , iy));
}

Of course in this simple example printf format requires type specification, as there is no equivalent of the println!() "{}" "type less" format specifier for printf. One could of course use overloads to achieve the same level of "generic" type insensitivity for the "display" code.

The stackoverflow was written before associated types were added [1].


  1. before Add was associated typed :wink: ↩ī¸Ž

1 Like

Thank you very much. Your solution was correct and provdies for the correct update to the stack overflow article that I will post!

Here's code using your solution:

use std::ops::Add;

fn add<T: Add<T>>(a: T, b: T) -> T::Output {
    a + b
}

fn main() {
    println!("i32 result = {}", add(10i32,5i32));
    println!("f32 result = {}", add(10.3939f32,5.39393f32));
}

And here's the output as desired:

i32 result = 15
f32 result = 15.78783

fn add<L: Add<R>, R>(lhs: L, rhs: R) -> L::Output {
    lhs + rhs
}

Is more general, for what it's worth.

8 Likes

Thank you for your reply. The link should work now.

Your solution was also right on target. I've since understood both your comments as well as some additional subtle points that have helped by other respondents here.

I appreciate all the feedback!

It's worth a lot. Thanks for the insight.

Generally, overloads fall into two basic categories:

  • Optional arguments
  • Handling multiple argument types.

The latter is handled with taking a trait, but the former is a bit trickier. The standard library and general convention is just have a different name, but with even a few options you might need to use a builder type as your argument, or instead of it: for example std::fs::OpenOptions

2 Likes

You should generally read the official Rust documentation (the Book and the Reference) instead of severely out-of-date Stack Overflow posts. The official learning resources are always up-to-date with the latest Rust release.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.