Generic Functions: C++ vs Rust

I'm wondering if there is a way to get Rust to work more like C++ when it comes to generics. In C++ the compiler is able to figure out if a generic type has a method or not, for that type. Rust doesn't appear to have this ability... but I'm hoping I'm missing something. Two examples in C++ and Rust:

class A {
public:
    string hello() { return "Hello from A"; }
};

class B {
public:
    string hello() { return "Hello from B"; }
};

template<typename T>
string call_hello(T instance) { return instance.hello(); }


int main() {
    cout << call_hello(A()) << endl;
    cout << call_hello(B()) << endl;
}
struct A { }
struct B { }

impl A {
    pub fn hello(&self) -> String {
        String::from("Hello from A")
    }
}

impl B {
    pub fn hello(&self) -> String {
        String::from("Hello from B")
    }
}

fn call_hello<T>(instance: &T) -> String {
    instance.hello()
}

fn main() {
    println!("{}", call_hello(&A {}));
    println!("{}", call_hello(&B {}));
}

The Rust code however fails to compile with:

error[E0599]: no method named `hello` found for type `&T` in the current scope
  --> src/main.rs:17:14
   |
17 |     instance.hello()
   |              ^^^^^

Am I missing something here? It feels like I am because it says, "in the current scope" which makes me think I need to somehow indicate to call_hello that these types have a method hello.

I know I can create a trait that specifies a function fn hello() -> String and then implement said trait for both A and B; however, the problem I'm actually trying to solve is creating a mock for a UdpSocket. However, UdpSocket doesn't have an associated trait for all the methods I care about (bind, recv_from, etc), and so I'd have to create one, then implement all those methods as pass-thru calls to the underlying UdpSocket, then do the same for my MockSocket. Of course if I change my code to call something like set_read_timeout, then I have to update my trait, my impl for UdpSocket and for my MockSocket. I was just hoping to side-step all of this by using a generic, and having it work like it does in C++. This way if my code starts using set_read_timeout, the compiler would only complain about me not having that implementation for MockSocket.

Thoughts? Thanks!

1 Like

Generics in Rust don't work by substitution of syntax. The "find'n'replace" kind of generics is easy to write, but in complex cases ends up giving hard to understand error messages, and makes it hard to declare exactly what types are supported and which aren't.

For Rust it's important that implementation detail can't accidentally break an interface, so it requires generics to use traits. When you have a generic type, you can only do things with it that you've required the type to support. If you start calling .bye() in your code, Rust will tell you to update the trait, so you know you're making a breaking change to the interface.

I'm afraid there's no good solution for mocks. You will need to have a trait (there's impl Trait syntax that makes converting non-trait code to traits easier). Maybe a macro could help reduce pain of copying the methods of the socket to the trait?

7 Likes

Or, to put it another way: C++ checks the validity of generics at use. Rust checks them at definition. There is no way around this.

Yes, you will have to define and implement a trait for what you're trying to do.

3 Likes

If you really want a C++ -like behavior for this, you can use a macro:

macro_rules! call_hello {
    ($instance:expr) => {
        $instance.hello();
    }
}

...

println!("{}", call_hello!(&A));

Playground

3 Likes

This statement is incorrect. The C++ compiler does not figure out whether a generic type has a method or not, it just assumes that it does. FWIW, this is why error messages involving generics in C++ are often really long and undecipherable, and why in Rust this is rarely the case.

If you want to emulate this in Rust you can just use declarative macros, but this is often seen as an anti-pattern because the error messages with macros are much worse than with generics.

5 Likes

NB. C++ doesn't have generics, it has templates, and the difference between generics and templates is exactly what other people in this thread have already described.

6 Likes

So I went the route of creating a trait that matches the UdpSocket functions I call, and then creating a mock socket that implements that trait: qcp/socket.rs at 36691ae363067f8a582c0ce59a291bf5eff937fa · wspeirs/qcp · GitHub

However, I'm facing an issue where when I try to proxy the function calls through to the real UdpSocket instance, the compiler complains about mis-matching types:

impl Socket for UdpSocket {
    fn bind<A: ToSocketAddrs + Debug, T: Socket + Send + Sync>(addr: A) -> io::Result<T> {
        return UdpSocket::bind(addr);
    }
...
}

Results in:

error[E0308]: mismatched types
  --> src/socket.rs:22:16
   |
22 |         return UdpSocket::bind(addr);
   |                ^^^^^^^^^^^^^^^^^^^^^ expected type parameter, found struct `std::net::UdpSocket`
   |
   = note: expected type `std::result::Result<T, _>`
              found type `std::result::Result<std::net::UdpSocket, _>`

What am I missing here? How do I tell the compiler that UdpSocket is a type T? Thanks!

bind() should be returning io::Result<Self>; drop the T generic arg to it.

Also, idiomatic Rust code would leave out the "return" in there, and instead use the expression itself, i.e.:

fn bind<A: ToSocketAddrs + Debug>(addr: A) -> io::Result<Self> {
    UdpSocket::bind(addr)
}
1 Like

@vitalyd, thanks for the Self advice... now I'm hung-up on creating an instance of T:

        let socket :T = T::bind(local_addr)?;

Results in:

error[E0282]: type annotations needed
  --> src/bbr_transport.rs:58:25
   |
58 |         let socket :T = T::bind(local_addr)?;
   |                         ^^^^^^^ cannot infer type for `T`

Same error if I replace T::bind(...) with Socket::bind(...). Help/thoughts? Thanks!

Is the Github code updated? It seems not because Socket::bind still has the same signature as before. Please update it or paste the relevant code here.

@vitalyd my apologies, pushed: https://github.com/wspeirs/qcp/blob/master/src/bbr_transport.rs#L58 Thanks!

I think you missed the following part from my previous suggestion:

You still have the T arg on bind(), it needs to go away.

Same with try_clone(), it needs to shed the <T: Socket> and return Result<Self> (and get a Self: Sized bound).

To this point, @wspeirs if you don't intend to use Socket trait objects, you can just require Sized on the trait itself so you don't need this bound on the 2 methods:

pub trait Socket: Sized { 
...
}

Fixed up a few other things, but got it compiling... thanks everyone!