How to call a method that consumes self on a boxed trait object?
Asked Answered
M

4

17

I have the following sketch of an implementation:

trait Listener {
    fn some_action(&mut self);
    fn commit(self);
}

struct FooListener {}

impl Listener for FooListener {
    fn some_action(&mut self) {
        println!("{:?}", "Action!!");
    }
    
    fn commit(self) {
        println!("{:?}", "Commit");
    }
}

struct Transaction {
    listeners: Vec<Box<dyn Listener>>,
}

impl Transaction {
    fn commit(self) {
        // How would I consume the listeners and call commit() on each of them?
    }
}

fn listener() {
    let transaction = Transaction {
        listeners: vec![Box::new(FooListener {})],
    };
    transaction.commit();
}

I can have Transactions with listeners on them that will call the listener when something happens on that transaction. Since Listener is a trait, I store a Vec<Box<Listener>>.

I'm having a hard time implementing commit for Transaction. Somehow I have to consume the boxes by calling commit on each of the stored Listeners, but I can't move stuff out of a box as far as I know.

How would I consume my listeners on commit?

Menorca answered 7/10, 2017 at 13:35 Comment(1)
Moving "stuff" out of a box is easy; you just dereference it. Your case is more complicated because you no longer know how big the value stored inside the box was. This means you get an error: cannot move a value of type Listener: the size of Listener cannot be statically determined.Cogency
R
20

Applying commit to the boxed object is not allowed because the trait object doesn't know its size (and it's not constant at compile-time). Since you plan to use listeners as boxed objects, what you can do is acknowledge that commit will be invoked on the box and change its signature accordingly:

trait Listener {
    fn some_action(&mut self);
    fn commit(self: Box<Self>);
}

struct FooListener {}

impl Listener for FooListener {
    fn some_action(&mut self) {
        println!("{:?}", "Action!!");
    }

    fn commit(self: Box<Self>) {
        println!("{:?}", "Commit");
    }
}

This enables Transaction to compile as you wrote it, because inside the implementation of FooListener the size of Self is well known and it is perfectly possible to move the object out of the box and consume both.

The price of this solution is that Listener::commit now requires a Box. If that is not acceptable, you could declare both commit(self) and commit_boxed(self: Box<Self>) in the trait, requiring all types to implement both, possibly using private functions or macros to avoid code duplication. This is not very elegant, but it would satisfy both the boxed and unboxed use case without loss of performance.

Ratcliffe answered 7/10, 2017 at 22:37 Comment(5)
Does this work for trait objects? Like, if I have a let box: Box<Listener>, can I call box.commit();?Hassett
I ask because Self is going to be FooListener, not Listener, correct?Hassett
@Hassett Yes, the answer (and likely the question) was written with trait objects in mind.Ratcliffe
Which types can be assigned to the first parameter self of struct methods? Is there any further statement about such usage? (I can't find it in the official tutorial.)Osteopath
@BobGreen See e.g. here. In summary, you can use Self optionally wrapped in a smart pointer like Box or Arc. See also my other answer which shows how to achieve the same without Box<Self>.Ratcliffe
R
5

The accepted answer shows what to do when you have the agency to modify the original Listener trait. If you don't have that option, i.e. if you control the Transaction type, but not Listener and its implementations, read on.

First we create a helper trait that is object-safe because none of its methods consume self:

trait DynListener {
    fn some_action(&mut self);
    fn commit(&mut self);
}

To use this trait everywhere Listener is usable, we will provide a blanket implementation of the trait. Normally such an implementation would implement DynListener for all types T: Listener. But that doesn't work here because Listener::commit() requires consuming self, and DynListener::commit() only receives a reference, so calling Listener::commit() would fail to compile with "cannot move out of borrowed content". To work around that, we implement DynListener for Option<T> instead. This allows us to use Option::take() to get an owned value to pass to Listener::commit():

impl<T: Listener> DynListener for Option<T> {
    fn some_action(&mut self) {
        // self is &mut Option<T>, self.as_mut().unwrap() is &mut T
        self.as_mut().unwrap().some_action();
    }

    fn commit(&mut self) {
        // self is &mut Option<T>, self.take().unwrap() is T
        self.take().unwrap().commit();
    }
}

DynListener::commit() takes value out of the Option, calls Listener::commit() on the value, and leaves the option as None. This compiles because the value is not "unsized" in the blanket implementation where the size of each individual T is known. The downside is that we are allowed to call DynListener::commit() multiple times on the same option, with all attempts but the first panicking at run time.

The remaining work is to modify Transaction to make use of this:

struct Transaction {
    listeners: Vec<Box<dyn DynListener>>,
}

impl Transaction {
    fn commit(self) {
        for mut listener in self.listeners {
            listener.commit();
        }
    }
}

fn listener() {
    let transaction = Transaction {
        listeners: vec![Box::new(Some(FooListener {}))],
    };
    transaction.commit();
}

Playground

Ratcliffe answered 9/11, 2021 at 17:17 Comment(0)
C
4

With the unsized_locals feature enabled, the natural code works as-is:

// 1.37.0-nightly 2019-06-03 6ffb8f53ee1cb0903f9d
#![feature(unsized_locals)]

// ...

impl Transaction {
    fn commit(self) {
        for l in self.listeners {
            l.commit()
        }
    }
}
Cogency answered 4/6, 2019 at 16:30 Comment(0)
R
1

According to user4815162342's answer, I wrote a macro to generate commit_boxed(self: Box<Self>) here.

Furthermore, after adding self: Box<Self> signature, you can easily implement trait Listener for Box<dyn Listener>. That means you can call box.commit() just as listener.commit().

impl Listener for Box<dyn Listener> {
    fn some_action(&mut self) {
        //...
    }
    fn commit(self) {
        Listener::commit_boxed(self)
    }
    fn commit_boxed(self:Box<Self>) {
        Listener::commit_boxed(*self)
    }
}
Ropeway answered 7/10, 2022 at 17:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.