Generic method vs type


#1

Why it’s not possible to use:

fn do_something(x: Foo) {
    println!("{}", x.method());
}

In this example:

trait Foo {
    fn method(&self) -> String;
}

impl Foo for u8 {
    fn method(&self) -> String { format!("u8: {}", *self) }
}

impl Foo for String {
    fn method(&self) -> String { format!("string: {}", *self) }
}

fn do_something<T: Foo>(x: T) {
    println!("{}", x.method());
}

fn main() {
    let x = 5u8;
    let y = "Hello".to_string();

    do_something(x);
    do_something(y);
}

#2

You can do something similar, using a trait object:

trait Foo {
    fn method(&self) -> String;
}

impl Foo for u8 {
    fn method(&self) -> String { format!("u8: {}", *self) }
}

impl Foo for String {
    fn method(&self) -> String { format!("string: {}", *self) }
}

fn do_something(x: &Foo) {
    println!("{}", x.method());
}

fn main() {
    let x = 5u8;
    let y = "Hello".to_string();

    do_something(&x);
    do_something(&y);
}

In general the object that implements Foo could have different binary sizes (1 byte and 24 bytes in your case on a 64 bit system), so a generic function like do_something(x: Foo) can’t work. There are other ways to solve the problem in other languages.


#3

There are two possible solutions:

Static dispatch: the function gets instantiated for every combination of type parameters. This is the most time-efficient approach.

fn do_something<F: Foo>(x: &F) {
    println!("{}", x.method());
}

Dynamic dispatch: hide the actual type behind a “fat pointer” (references and Boxes are pointers too) which includes a pointer to the vtable and a pointer to the data. This is called a trait object. It is more space-efficient but less time-efficient as it needs to look up the right function in the vtable at runtime. Some limitations apply to which traits can be turned into a trait object. Generally useful for heterogeneous collections.

fn do_something(x: &Foo) {
    println!("{}", x.method());
}

Most of the time static dispatch is preferred.


#4

Is it really caused by different binary size of variables? The following doesn’t work as well:

let x = 5u8;
let y = 5u8;

do_something(x);
do_something(y);

#5

Rust is quite specific about this. To have a generic function, it must be declared with type parameters. That’s the fn do_something<T: Foo>(x: T) declaration.

Beyond that, rust gives a meaning to Foo as a type (not just as a trait), being the object type of the trait of the same name. fn do_something(x: Foo) then has a different meaning than the generic function (the argument is of a trait object type).

As of this writing, rust does not support trait objects by value, so there is no way to create or pass a value of type Foo, but as the others showed, trait objects can be used today when the object type is behind a pointer. (&Foo and so on).

Your proposed syntax will be legal when passing trait objects by value is implemented, but it will then have a meaning that’s distinct from how the generic function works.

A function is not generic without type parameters (or lifetime parameters).


#6

I probably got it. The generic function is compiled into several instances according to types it was used with. So isn’t it better to use it without & ?

fn do_something<T: Foo>(x: T) {
    println!("{}", x.method());
}
fn main() {
    let x = 5u8;
    let y = "Hello".to_string();

    do_something(x);
    do_something(y);
}

#7

In most cases generics are simpler and easier to use.


#8

I meant & in Jasha’s Static dispatch example where he has fn do_something<F: Foo>(x: &F).


#9

The reason I used & is to have the static dispatch function be analogous to the dynamic dispatch function, i.e. they both take a shared reference to the argument instead of taking ownership of the argument.

Since all your do_something function does is call Foo::method on the argument by reference (&self) and print the result it seems unnecessary to consume the Foo argument. Calling do_something(x) twice using your definition (fn do_something<T: Foo>(x: T) { ... }) would work because u8 implements Copy and thus uses copy semantics. However, calling do_something(y) twice would not work because String is not Copy and uses move semantics which would consume the String after the first function call. To me this seems like an unnecessary restriction given your implementation of do_something.


#10

Thanks for explanation guys. I modified the existing example to this:

trait Foo {
    fn method(&self) -> String;
}

impl Foo for u8 {
    fn method(&self) -> String { format!("u8: {}", *self) }
}

impl Foo for &'static str {
    fn method(&self) -> String { format!("string: {}", *self) }
}

impl Foo for String {
    fn method(&self) -> String { format!("string: {}", *self) }
}

fn static_dispatch<T: Foo>(x: T) {
    println!("{}", x.method());
}

fn dynamic_dispatch(x: &Foo) {
    println!("{}", x.method());
}

fn main() {
    static_dispatch(5u8);
    static_dispatch("Static");
    static_dispatch("Hello".to_string());

    dynamic_dispatch(&5u8);
    dynamic_dispatch(&"Static");
    dynamic_dispatch(&"Hello".to_string());
}

and more questions came to my mind:

  1. The asterisk as prefix of self doesn’t have to be there. Does it change something?
  2. How do the values and objects are passed to generated functions in real? E.g. calling static_dispatch("Static"); copies whole data of string or passes them as pointer? Are &5u8 or &"Static" boxed to heap and then fat pointer is passed as Jascha has written or the mechanism is different?
  3. I suppose that "Hello".to_string() is fat pointer already (an object) so why there is need to add &.

#11
  1. println! can deal with references because there are generic impls for &T that format a T.
  2. static_dispatch("Static") doesn’t copy a string (type is &'static str), it passes a pointer to the char constant in the data section. With static_dispatch("Hello".to_string()) the type is String (no pointer), but string data is allocated on the heap, so only a small structure with (pointer, length, capacity) is moved into the function.
    As for fat pointers, &5u8 or "Static" (which is already a pointer) are ordinary pointers. Fat pointers as Jascha said are only involved in trait objects, when you have a type &Trait or Box<Trait>. Rust creates them automatically from ordinary pointers when calling functions that take trait objects.
  3. A fat pointer has nothing to do with being an “object”. "Hello".to_string() returns an owned string (type String) while the dynamic_dispatch requires a pointer, so you use & to create a pointer.

#12

Do you mean &'static str or &'static String?


#13

In static_dispatch("Static") the argument is a &'static str.


#14

Fixed.