Automatic boxing for receiver Box<Self>

Problem

In Rust, taking ownership of an object is a way of enforcing proper API usage. It may be possible to implement a particular method using only &mut self or perhaps even &self, but if the library author wants to ensure the object won't be used again after the API call, they'll have the method take an self instead.

Currently, the only way for an object-safe trait to take ownership of Self is via Box<Self>:

trait A {
    fn borrow(&self, ...);          // Object safe
    fn borrow_mut(&mut self, ...);  // Object safe
    // fn owned(self, ...);         // Not object safe*. Requires Self: Sized

    fn owned(self: Box<Self>, ...);
}

Unfortunately, this has poor ergonomics for users of the trait who don't care about object safety. If they have a Sized type that implements A, calling owned required them to do Box::new(x).owned().

This leaves library authors with a couple of choices:

  • Make their trait take self and forgo object safety.
  • Take an &mut self and add runtime checks to ensure the object isn't used afterwards.
  • Use Box<Self> and accept the worse experience for users who don't need object safety.
  • Suggested by @Nemo157: Have two differently named versions of owned and force users to pick the right one based on the context. Non-object safe version could have a simple implementation that just called the Box version.

*There is a proposal to allow unsized function arguments but it is likely a long ways out.

Possible solution

A small amount of syntactic sugar could make the situation much nicer. Have the dot-operator automatically call Box::new for receiver Box methods, the same way it does autoref currently.

This would imply "implicit" allocations when viewing the call site. However, the function signature would still be clear about the possible allocation (which is more than can be said about many existing functions that allocate immediately after being called!)

Also if desired, the possible allocation could be made even more clear by adding a #[autobox] (or similar) annotation to the trait definition.

There's another option you didn't list: have an extra method for the non-object-safe owned variant

trait A {
    fn owned(self: Box<Self>, ...);
    fn non_object_safe_owned(self, ...) where Self: Sized {
        Box::new(self).owned(...)
    }
}

(and this lets implementors override it if they can do it more efficiently, and maybe have owned delegate via downcasting)

4 Likes

I added your suggestion to the list of options, but to be clear about the downside: it forces many users to write x.non_object_safe_owned() instead of just x.owned(). You could swap the names and make the non-object safe version the "nice" one, but you'd still need to add a bunch of documentation explaining that sometimes users have to call the other method.

And regardless, you'd now have two functionality equivalent methods in your rustdoc output, code completion, etc. Potentially hard to justify if object safety isn't that important

(What I'd really love is an object safe option that was generic over the two versions, but that seems like a much larger language addition.)

This generally shouldn't need to be true. Either users don't own an instance of the trait by value, so they wouldn't be able to call it anyway, by design, or they do own an instance by value and can call the method. e.g.

trait A {
    fn consume(self)
    where Self: Sized
    {
        Box::new(self).consume_by_box()
    }

    fn consume_by_box(self: Box<Self>);
}

impl<T: ?Sized + A> A for Box<T> {
    fn consume(self) {
        T::consume_by_box(self)
    }

    fn consume_by_box(self: Box<Self>) {
        T::consume_by_box(*self)
    }
}

It certainly needs to be documented for implementors, but consumers simply can't own an instance of the trait by value which doesn't satisfy Self: Sized.

3 Likes

The impl<T: ?Sized + A> A for Box<T> was the part I was missing. Thanks!

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.