Why does a general method inside a stroke require the size of an object?

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 create a polymorphic object (trait Messenger and one of the polymorphic implementations of 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 found that in this case I should require Sized , but that will 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:

  • Why do we need Sized here? This violates polymorphism if we cannot use this method from object to object.
  • In fact, we cannot use this method from Arc<Messenger> , and 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 completely stuck here. I do not know how to use polymorphism with a common method in a trait. Is there any way?

+5
source share
3 answers

Dynamic dispatch (i.e. calling methods through object objects) works by calling via vtable (for example, using a function pointer), since you do not know at compile time what function will be.

But if your function is generic, it needs to be compiled differently (monomorphically) for each instance of F that is actually used. This means that you will have a different copy of send_embed for each type of closure he called with. Each close is a different type.

These two models are incompatible: you cannot have a pointer to a function that works with different types.

However, you can change the method to use the object object, and not to compile:

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

( Playground )

Instead of another send_embed for each type, which can be Fn(String) -> String , it now accepts a reference to the object of the object. (You can also use Box<Fn()> or similar). You need to use Fn or FnMut , not FnOnce , since the latter takes the value of self by value, that is, it is also not safe for the object (the caller does not know what size will pass as the closure of self ).

You can still call send_embed with the close / lambda function, but it just has to be by reference, for example:

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

I updated the playground to include an example call to send_embed directly with a link to close, as well as an indirect route through a common shell on Bot .

+9
source

Traits and Traits

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

  • related types
  • related constants
  • related functions.

and you can use the following features:

  • as compilation time limits for general parameters
  • as types, behind links or pointers.

However ... only some traits can be used directly as types. Those traits that have the designation Object Safe.

It is now considered unsuccessful that there is one trait keyword to define both fully functional and object-safe attributes.


Interlude: How does dispatch at runtime work?

When using a feature as type: &Trait , Box<Trait> , Rc<Trait> , ... at run time, a thick pointer is used consisting of:

  • data pointer
  • virtual pointer.

A method call is sent through a virtual pointer to a virtual table.

For such characteristics as:

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

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

Thus, scheduling of runtime is performed:

  • Choosing the right table element
  • calling it a pointer and data arguments.

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

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

Why can't I use any attribute as a type? Which object is safe?

This is a technical limitation.

There are many features features that cannot be implemented for runtime mailings:

  • related types
  • related constants
  • related common functions
  • related functions with Self in signature.
  • ... maybe others ....

There are two ways to signal this problem:

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

For now, Rust prefers to signal the problem earlier: traits that do not use any of the above functions are Object Safe calls and can be used as types.

Non-object safety features cannot be used as types, and an error is triggered immediately.


Now what?

In your case, just 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 are a few wrinkles: FnOnce requires moving from f , and it is borrowed here, so instead you need to use FnMut or Fn . FnMut is the following more general method, therefore:

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

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

+12
source

The general method cannot be made object-safe , because you cannot use vtable with it. The @ChrisEmerson answer explained in detail why.

In your case, you can make the object attribute send_embed by making f take the object object instead of the general parameter. If your function accepts f: F where F: Fn(X) -> Y , you can force it to accept f: &Fn(X) -> Y , similarly for FnMut f: &mut FnMut(X) -> Y FnOnce is more complicated since Rust does not support moving non-standard types, but you can try it:

 // ↓ no generic ↓~~~~~~~~~~~~~~~~~~~~~~~~~~~~ box the closure fn send_embed(&self, u64, &str, f: Box<FnOnce(String) -> String>) -> Option<u64> where Self: Sync + Send { f("hello".to_string()); None } b.messenger.send_embed(1, "234", Box::new(|a| a)); // note: does not work. 

However, with Rust 1.17.0 you cannot insert FnOnce and call it , you need to use FnBox :

 #![feature(fnbox)] use std::boxed::FnBox; // ↓~~~~ fn send_embed(&self, u64, &str, f: Box<FnBox(String) -> String>) -> Option<u64> where Self: Sync + Send { f("hello".to_string()); None } b.messenger.send_embed(1, "234", Box::new(|a| a)); 

If you do not want to use the unstable function, you can use the boxfnonce box as a workaround:

 extern crate boxfnonce; use boxfnonce::BoxFnOnce; fn send_embed(&self, u64, &str, f: BoxFnOnce<(String,), String>) -> Option<u64> where Self: Sync + Send { f.call("hello".to_string()); None } b.messenger.send_embed(1, "234", BoxFnOnce::from(|a| a)); 
+4
source

Source: https://habr.com/ru/post/1265075/


All Articles