Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using callbacks on trait objects

Tags:

rust

I’m trying to use a callback function on a trait object. I reduced my problem to the following code (playpen):

trait Caller {
    fn call(&self, call: fn(&Caller)) where Self: Sized {
        call(self)
    }
}

struct Type;
impl Caller for Type {}

fn callme(_: &Caller) {}

fn main() {
    let caller: Box<Caller> = Box::new(Type);
    caller.call(callme); // does not work
    //callme(&*caller);  // works
}

which results in

<anon>:14:12: 14:24 error: the trait `core::marker::Sized` is not implemented for the type `Caller` [E0277]
<anon>:14     caller.call(callme); // does not work
                     ^~~~~~~~~~~~

Adding a Sized bound to Caller results in:

<anon>:3:14: 3:18 error: cannot convert to a trait object because trait `Caller` is not object-safe [E0038]

I really don’t understand why I need the Sized bound on the trait. Funnily it works if I use the callback directly. How do I get this to work?

Edit: Thanks to the answer I now came up with a nice solution

trait Caller {
    fn borrow(&self) -> &Caller;
    fn call(&self, call: fn(&Caller)) {
        call(self.borrow())
    }
}

struct Type;
impl Caller for Type {
    fn borrow(&self) -> &Caller { self }
}

fn callme(_: &Caller) {}

fn main() {
    let caller: Box<Caller> = Box::new(Type);
    caller.call(callme);
}
like image 712
Ferio Avatar asked Oct 20 '22 16:10

Ferio


1 Answers

The argument call in fn call takes a trait object &Caller, so calling it requires coercing the self reference (of type &Self) to a &Caller trait object. The coercion is only possible when &Self is a thin pointer rather than a fat pointer like a trait object or a &[T] slice. &Self is a thin pointer exactly when Self: Sized. The compiler defaults to Self in traits not being Sized, and so the extra restriction is required. The Sized trait represents that the type has a size that is known at compile time, there is no need to store extra info (next to the pointer, making it "fat") to compute it at runtime.

Unfortunately, this leaves a hole: AFAIK, it's actually not possible to have such a method be a default method and still be able to call it on trait objects, since a trait object &Caller has Self = Caller which isn't Sized. However, it should work if the method is implemented manually for each type:

trait Caller {
    fn call(&self, call: fn(&Caller));
}

struct Type;
impl Caller for Type {
    fn call(&self, call: fn(&Caller)) {
        call(self)
    }
}

fn callme(_: &Caller) {}

fn main() {
    let caller: Box<Caller> = Box::new(Type);
    caller.call(callme);
}

The call method declaration in the trait no longer needs the where Self: Sized since it isn't trying to do the trait object coercion itself, and the concrete implementations have much more control over how the &Caller trait object is obtained. For Sized types, it works directly, like the original where Self: Sized code.

like image 73
huon Avatar answered Oct 22 '22 21:10

huon