This topic is about ergonomics around cloning trait objects, but it also applies to other operations on trait objects. The more I think about this topic, the more questions pop up. Is there any RFC out there, that has already answered all / most of these questions, that may have even been approved, already, but not implemented, yet? I think most (if not all) people working with trait objects would appreciate better (and especially more safe) support at the language level.
The following code is copied from dyn-clone (including the example):
use crate::sealed::{Private, Sealed};
mod sealed {
pub trait Sealed {}
impl<T: Clone> Sealed for T {}
pub struct Private;
}
pub trait DynClone: Sealed {
// Not public API
#[doc(hidden)]
fn __clone_box(&self, _: Private) -> *mut ();
}
pub fn clone_box<T>(t: &T) -> Box<T>
where
T: ?Sized + DynClone,
{
let mut fat_ptr = t as *const T;
unsafe {
let data_ptr = &mut fat_ptr as *mut *const T as *mut *mut ();
assert_eq!(*data_ptr as *const (), t as *const T as *const ());
*data_ptr = <T as DynClone>::__clone_box(t, Private);
}
unsafe { Box::from_raw(fat_ptr as *mut T) }
}
impl<T> DynClone for T
where
T: Clone,
{
fn __clone_box(&self, _: Private) -> *mut () {
Box::into_raw(Box::new(self.clone())) as *mut ()
}
}
trait MyTrait: DynClone {
fn recite(&self);
}
impl MyTrait for String {
fn recite(&self) {
println!("{} ?", self);
}
}
fn main() {
let line = "The slithy structs did gyre and gimble the namespace";
// Build a trait object holding a String.
// This requires String to implement MyTrait and std::clone::Clone.
let x: Box<dyn MyTrait> = Box::new(String::from(line));
let x_ref: &dyn MyTrait = &*x;
x.recite();
// The type of x2 is a Box<dyn MyTrait> cloned from x.
let x2 = clone_box(x_ref);
x2.recite();
}
That this works is actually quite interesting and makes me wonder why there is no easier way to achieve this. The part that strikes me as surprising is <T as DynClone>::__clone_box(t, Private)
in fn clone_box<T>
. T
may be Sized
and DynClone
may be Sized
, as well, but DynClone
is only implemented for Sized
types, which begs the question why the compiler doesn't throw an error when trying to attempt to call the function with an unsized type.
Since this code compiles, why shouldn't the following code compile, too? (I left out anything, that I didn't change)
// [...]
pub trait DynClone: Clone + Sealed {
// Not public API
#[doc(hidden)]
fn __clone_box(&self, _: Private) -> *mut () {
Box::into_raw(Box::new(self.clone())) as *mut ()
}
}
impl<T: Clone> DynClone for T {}
// [...]
If it compiled, this should behave the same as the previous example. This means, having a super-trait, that is Sized
should not necessarily prevent object safety. It might be treated like a trait with all methods being marked with where Self: Sized
[1], but that's not all there is to it. The first example also shows, that it should be possible to call where Self: Sized
methods from where Self: ?Sized
methods, if the only reason they're not object-safe is, that they return Self
(this could probably be more relaxed; the example simply doesn't prove anything else). In this example, -> Self
would be treated like -> impl DynClone
, being comparable to -> impl Iterator
which is in use, today.
[1] This will likely be confusing, though, since Sized is currently inheritable, while it wouldn't be in my example. This seems like a stability issue and is probably the answer to the question, why it wouldn't work. Maybe, it requires some additional syntax to be explicit about it?
This opens up another question. Why wouldn't we be able to just write the following?
pub trait DynClone: Clone + Sealed {
fn clone_box(&self) -> Box<dyn Self> {
Box::new(self.clone())
}
}
This would be performing the (unsafe) operations from the free-standing clone_box
, without having to call it. That would make it more ergonomic to use.
The end goal would be for Clone
in the standard library to expose this behavior, instead of having this be a feature of an external crate. If one deals with a Box<dyn {trait: Clone}>
, one would never be able to call the regular clone method. If we could somehow re-use the same name, though, instead of having to use myfn_box
everywhere for the polymorphic counterpart, that would be great. That means, calling Clone::clone
on a sized type yields T
while calling it on a DST yields Box<dyn <T as {trait: Clone}>>
or similar.
pub trait Clone {
fn clone(&self) -> Self where Self: Sized { ... }
}
// Kinda what I imagine, but obviously doesn't work, at the moment.
// Perhaps, it's not even the best approach
impl dyn Clone {
pub fn clone(&self) -> Box<Self> {
Box::new(Clone::clone(&self))
}
}
However, being stuck with Box
in a generic context is questionable, because it might be appropriate for other heap-allocating structures or custom allocators to be used, instead. Custom structures may have different internal layout requirements (minimum alignment) which do not allow conversion from Box<T>
without reallocation.
This is where I'm currently stuck at. All of the proposed solutions don't really work out of the box, either. The examples are just conceptual, not how I imagine it'd actually look like, if it would become part of the language.
All in all, I'm neither satisfied with what Rust currently offers nor am I able to work out a solution I can proudly present as the solution to the problem at hand. This makes me unhappy, because I'm of the opinion, that if one wants to negatively critize someone/something, they should also invest some time to come up with an advice on how to improve, but I just can't come up with anyting that really works, yet and also didn't want to wait until I did, because who knows how long that will take.