Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does a generic method inside a trait require trait object to be sized?

Tags:

generics

rust

I have this code (playground):

use std::sync::Arc;

pub trait Messenger : Sync + Send {
    fn send_embed<F: FnOnce(String) -> String>(&self, u64, &str, f: F)
        -> Option<u64> where Self: Sync + Send;
}

struct MyMessenger {
    prefix: String,
}
impl MyMessenger {
    fn new(s: &str) -> MyMessenger {
        MyMessenger { prefix: s.to_owned(), }
    }
}
impl Messenger for MyMessenger {
    fn send_embed<F: FnOnce(String) -> String>(&self, channel_id: u64, text: &str, f: F) -> Option<u64> {
        println!("Trying to send embed: chid={}, text=\"{}\"", channel_id, text);
        None
    }

}

struct Bot {
    messenger: Arc<Messenger>,
}
impl Bot {
    fn new() -> Bot {
        Bot {
            messenger: Arc::new(MyMessenger::new("HELLO")),
        }
    }
}

fn main() {
    let b = Bot::new();
}

I wanted to make a polymorphic object (trait Messenger and one of polymorphic implementations is MyMessenger). But when I try to compile it I have an error:

error[E0038]: the trait `Messenger` cannot be made into an object
  --> <anon>:25:5
   |
25 |     messenger: Arc<Messenger>,
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Messenger` cannot be made into an object
   |
   = note: method `send_embed` has generic type parameters

I have found that I must require Sized in this case, but this does not solve it. If I change my send_embed method to the following:

fn send_embed<F: FnOnce(String) -> String>(&self, u64, &str, f: F)
    -> Option<u64> where Self: Sized + Sync + Send;

Then it compiles successfully but:

  1. Why do we need Sized here? This violates polymorphism if we can not use this method from a trait object.
  2. We actually can't use this method from Arc<Messenger> then:

    fn main() {
        let b = Bot::new();
        b.messenger.send_embed(0u64, "ABRACADABRA", |s| s);
    }
    

    Gives:

    error[E0277]: the trait bound `Messenger + 'static: std::marker::Sized` is not satisfied
      --> <anon>:37:17
       |
    37 |     b.messenger.send_embed(0u64, "ABRACADABRA", |s| s);
       |                 ^^^^^^^^^^ the trait `std::marker::Sized` is not implemented for `Messenger + 'static`
       |
       = note: `Messenger + 'static` does not have a constant size known at compile-time
    

I am totally stuck here. No idea how to use polymorphism with generic method in a trait. Is there a way?

like image 863
Victor Polevoy Avatar asked Mar 06 '17 07:03

Victor Polevoy


People also ask

What is sized trait rust?

The Sized trait in Rust is an auto trait and a marker trait. Auto traits are traits that get automatically implemented for a type if it passes certain conditions. Marker traits are traits that mark a type as having a certain property.

What is a trait object?

A trait object is an opaque value of another type that implements a set of traits. The set of traits is made up of an object safe base trait plus any number of auto traits. Trait objects implement the base trait, its auto traits, and any supertraits of the base trait.

What is object safety in Rust?

Object SafetyOnly traits that are object-safe can be made into trait objects. A trait is object-safe if both of these are true: the trait does not require that Self: Sized. all of its methods are object-safe.


2 Answers

Traits and Traits

In Rust, you can use trait to define an interface comprised of:

  • associated types,
  • associated constants,
  • associated functions.

and you can use traits either:

  • as compile-time bounds for generic parameters
  • as types, behind references or pointers.

However... only some traits can be used directly as types. Those traits that do are labeled Object Safe.

It is now considered unfortunate that a single trait keyword exists to define both full-featured and object-safe traits.


Interlude: How does run-time dispatch work?

When using a trait as a type: &Trait, Box<Trait>, Rc<Trait>, ... the run-time implementation uses a fat pointer composed of:

  • the data pointer,
  • the virtual pointer.

Method calls are dispatched through the virtual pointer to a virtual table.

For a trait like:

trait A {
    fn one(&self) -> usize;
    fn two(&self, other: usize) -> usize;
}

implemented for type X, the virtual table will look like (<X as A>::one, <X as A>::two).

The run-time dispatch is thus performed by:

  • picking the right member of the table,
  • calling it with the data pointer and arguments.

This means that <X as A>::two looks like:

fn x_as_a_two(this: *const (), other: usize) -> usize {
    let x = unsafe { this as *const X as &X };
    x.two(other)
}

Why cannot I use any trait as a type? What's Object Safe?

It's a technical limitation.

There are a number of traits capabilities that cannot be implemented for run-time dispatches:

  • associated types,
  • associated constants,
  • associated generic functions,
  • associated functions with Self in the signature.
  • ... maybe others ....

There are two ways to signal this issue:

  • early: refuse to use a trait as a type if it has any of the above,
  • late: refuse to use any of the above on a trait as a type.

For now, Rust chooses to signal the issue early on: traits that do not use any of the above features are call Object Safe and can be used as types.

Traits that are not Object Safe cannot be used as types, and an error is immediately triggered.


Now what?

In your case, simply switch from compile-time polymorphism to run-time polymorphism for the method:

pub trait Messenger : Sync + Send {
    fn send_embed(&self, u64, &str, f: &FnOnce(String) -> String)
        -> Option<u64>;
}

There is a little wrinkle: FnOnce requires moving out of the f and it's only borrowed here, so instead you need to use FnMut or Fn. FnMut is next more generic method, so:

pub trait Messenger : Sync + Send {
    fn send_embed(&self, u64, &str, f: &FnMut(String) -> String)
        -> Option<u64>;
}

This makes the Messenger trait Object Safe and therefore allows you to use a &Messenger, Box<Messenger>, ...

like image 149
Matthieu M. Avatar answered Sep 30 '22 05:09

Matthieu M.


Dynamic dispatch (i.e. calling methods through trait objects) works by calling through a vtable, (i.e. using a function pointer), since you don't know at compile time which function it will be.

But if your function is generic, it needs to be compiled differently (monomorphised) for every instance of F which is actually used. Which means you'll have a different copy of send_embed for every different closure type it's called with. Every closure is a different type.

These two models are incompatible: you can't have a function pointer which works with different types.

However, you can change the method to use a trait object as well instead of being compile-time generic:

pub trait Messenger : Sync + Send {
    fn send_embed(&self, u64, &str, f: &Fn(String) -> String)
        -> Option<u64> where Self: Sync + Send;
}

(Playground)

Instead of a different send_embed for every type which can be Fn(String) -> String, it now accepts a trait object reference. (You could also use a Box<Fn()> or similar). You do have to use Fn or FnMut and not FnOnce, since the latter takes self by value, i.e. it's also not object safe (the caller doesn't know what size to pass in as the closure's self parameter).

You can still call send_embed with a closure/lambda function, but it just needs to be by reference, like this:

self.messenger.send_embed(0, "abc", &|x| x);

I've updated the playground to include an example of calling send_embed directly with a referenced closure, as well as the indirect route through a generic wrapper on Bot.

like image 32
Chris Emerson Avatar answered Sep 30 '22 04:09

Chris Emerson