I am trying to overload member functions of my class (similar to what can be done in C++). So I read that in Rust one has to use traits to achieve this. Below is some sample code (note, this is just to demonstrate the idea):
/* my_class.rs */
pub struct MyClass {
pub a: i32,
pub b: i32,
}
pub trait Func<T> {
fn func(&self, t: T) -> Self;
}
impl Func<i32> for MyClass {
fn func(&self, t: i32) -> MyClass {
MyClass { a: self.a + t, b: self.b }
}
}
impl Func<&str> for MyClass {
fn func(&self, t: &str) -> MyClass {
MyClass { a: self.a, b: self.b + t.parse::<i32>().unwrap() }
}
}
and
/* main.rs */
mod my_class;
use crate::my_class::MyClass;
use crate::my_class::Func;
fn main() {
let m1 = MyClass {a: 10, b:20}.func(5);
let m2 = MyClass {a: 10, b:20}.func("-8");
println!("a={}, b={}", m1.a, m1.b);
println!("a={}, b={}", m2.a, m2.b);
}
Firstly, is this the correct way to overload class-member functions? It seems a little cumbersome, as one needs to add boilerplate pub trait Func<T>
for every function overload.
And secondly, is there a way so that I do not have to write use crate::my_class::Func;
for every trait? That is, how can I bring all functions of MyClass
(both defined via impl MyClass
and impl Func<T> for MyClass
) into scope when I import MyClass
?
If you want to emulate full function overloading, then yes, traits are the way to go. If you don't want to import them, you could just put all your related traits in the same module as the struct then import them all with use crate::my_class::*
.
But don't. There are a lot of reasons why C++/Java-style function overloading isn't a good idea:
.func(5)
would add 5
to a
, while .func("5")
would add 5
to b
.foo
that takes in some T
that can be passed to func
. How would you bound it? It'd look something like this:fn foo<T>
where
MyClass: Func<T>
{ unimplemented!() }
This is already kind of ugly. Now imagine you have a MyClass2
that has an overloaded function that also takes in an int-like value (i32
or &str
that can be parsed to an i32
). Your bound now looks like this:
fn foo<T>
where
MyClass: Func<T>,
MyClass2: Func<T>
{ unimplemented!() }
even though they're conceptually the same bound on int-like values. When more generics and overloads get added, it only gets uglier and uglier.
a
, and one value to add to b
. You now need 4 implementations:fn func(&self, t1: i32, t2: i32) -> Self;
fn func(&self, t1: i32, t2: &str) -> Self;
fn func(&self, t1: &str, t2: i32) -> Self;
fn func(&self, t1: &str, t2: &str) -> Self;
What if you want to support i64
as well? Now you have 9 implementations. And if you want to add a third argument? Now you have 27.
All of these issues stem from the fact that, conceptually, the bound is really on the argument instead of the function. So, write the code to match the concept and put the trait bound on the argument instead of the function. It prevents confusing overloads that do fundamentally different operations, takes the burden of use off the caller, and stops implementations from exponentially exploding. Best of all, the trait doesn't even need to be imported for the method to be used. Consider this:
/* my_class.rs */
pub struct MyClass {
pub a: i32,
pub b: i32,
}
pub trait IntLike {
fn to_i32(self) -> i32;
}
impl IntLike for i32 {
fn to_i32(self) -> i32 { self }
}
impl IntLike for &str {
fn to_i32(self) -> i32 { self.parse().unwrap() }
}
impl MyClass {
pub fn func<T: IntLike>(&self, t: T) -> Self {
Self { a: self.a + t.to_i32(), b: self.b }
}
}
and
/* main.rs */
mod my_class;
// don't even need to import the trait
use crate::my_class::MyClass;
fn main() {
let m1 = MyClass {a: 10, b:20}.func(5);
let m2 = MyClass {a: 10, b:20}.func("-8");
println!("a={}, b={}", m1.a, m1.b);
println!("a={}, b={}", m2.a, m2.b);
}
Isn't that just nicer?
i32
. You could refactor the shared code to a separate function then just call that function from the overloaded ones, but isn't that literally what bounding the argument on a trait does?BigInt
type, and they want it to work with your function. They'd have to import your trait, then copy-paste your implementation, and change one line to convert their BigInt
to an i32
. Not only is this ugly, it's actually impossible if your implementation references any private methods or properties. As the cherry on top, if you change your implementation (and its 20 overloads) to fix a bug, the developer of the external crate now needs to manually add in the bug fix. Other developers shouldn't ever have to concern themselves with your internals. An IntLike
trait would allow other developers to just handle the logic of converting to an i32
, then let you handle the rest.Debug
and Clone
. In fact, most of the time, you won't even have to make your own trait since there'll already be a trait for it.TL;DR idiomatic C++ is horrendous Rust.