Why can't traits require Clone?

  1. Minimal failure case:
// == works ===

pub trait CatT where
{
}


pub struct Cat {
    pub cat: Box<CatT>
}



// == fails ===


pub trait DogT where
Self: Clone
{
}


pub struct Dog {
    pub dog: Box<DogT>
}

  1. Question: Why can't add "Self: Clone" to DogT? I'd like to require that all impls of DogT to support clone.
1 Like

Because Clone is not object safe. Due to it requiring Sized.

1 Like

What would Box<DogT>::clone return? For any T: Clone + DogT, T::clone() returns a T. This is not portable to trait objects.

The solution is to instead introduce your own clone method, properly returning a trait object. You can even implement it automatically for everything that's Clone, you just need to make that explicit.

trait DogClone {
    fn dog_clone(&self) -> Box<DogT>;
}
impl<T> DogClone for T where T: DogT + Clone {
    fn dog_clone(&self) -> Box<DogT> {
        Box::new(self.clone())
    }
}

You'll probably want to mess with this a bit to get the exact semantics you want, but it's possible. The trick is that the compiler won't do any autobatic boxing for you - it won't turn T into Box<DogT> automatically in clone(). You have to write the code which does that yourself.

2 Likes

This could be extended to the general case with

trait BoxedClone<Inner: ? Sized> {
    fn boxed_clone(&self) -> Box<Inner>;
}

impl<T: Clone> BoxedClone<T> for T {
    fn boxed_clone(&self) -> Box<T> { Box::new(self.clone()) }
}

impl <T: DotT + Clone> BoxedClone<dyn DogT> for T {
    fn boxed_clone(&self) -> Box<dyn DogT> {
        Box::new(self.clone())
    }
}
2 Likes

This is technically correct and similar to what the rustc compiler complains about, but I have no idea what it means. Can you elaborate on this?

What does " 'Clone' is not object safe " mean? And how is it related to Sized?

2 Likes

I have no idea. I don't also understand why you are asking me this question.

Clone is on DogT. Clone is not on Box.

(This minimal failure case is extracted from more complex code).

Why is it taht adding "Clone" to DogT requires that Box::clone have a reasonable meaning? [Not rhetorical question. I'm genuinely confused.]

1 Like

Ah, sorry, I think I assumed too much.

When we create Box<DogT>, DogT is unsized. Fro here on out I'll use the new nomenclature dyn DogT to refer to this unsized dynamic trait. There can't exist a value of dyn DogT on the stack because it could be any number of different strict, all with different sizes, and the stack needs fixed sized values.

Your original code works without DogT: Clone specifically because you have a value Box<dyn DogT>, not dyn DogT. The box has a fixed size, and the dynamic size is hidden in an allocation. dyn DogT: Clone would similarly fail because you would need a single concrete function <dyn DogT>::clone which returns a dyn DogT. But dyn DogT cannot exist without a box or other indirection because it does not have a fi ed size. Thus this function cannot exist.

I focused on Box<dyn DogT>::clone because while it doesn't exist, a function like it could. Since it would return a Box<dyn DogT>, not a raw dyn DogT, the return value could actually exist on the stack and the definition could work.

If you haven't read it already, this book chapter section will probably be helpful? Advanced Types - The Rust Programming Language. It doesn't talk about sized in relation to traits but it might give some background knowledge in a more structured way than I can present it.

4 Likes

@zeroexcuses
Could you please tell us your background, so that I or someone else can describe you the issue and possible solutions in terms more familiar to you?

1 Like

I come from a background of C, C++, Clojure, Kotlin. I typed in all the examples in "Rust by Example" , switched over to Rust, and started asking questions in this forum. I have not read the "Rust Book"

Put it another way, are the following statements correct?

  1. DogT: Clone would require a function clone :: &dyn DogT -> dyn DogT.

  2. Such a function can not exist as we do not know the size of dyn DogT at compile time, since many different structs could implement trait DogT

===

If so, that is where I screwed up, because I (incorrectly) thought DogT: Clone meant:

forall struct S,
  impl DogT for S implies S has a clone()
3 Likes

Put it another way, are the following statements correct?

  1. DogT: Clone would require a function clone :: &dyn DogT -> dyn DogT .
  2. Such a function can not exist as we do not know the size of dyn DogT at compile time, since many different structs could implement trait DogT

Basically yes. In terms of C++,

struct Clonable {
    // Let's assume we have self-templates
    template<typename Self>
    Self clone(this Self const& self) const;
};

struct DogT: public Clonable {
    // clone cannot be made virtual here, because it's dependent on self-type returned by value
    virtual void say() = 0;
};

===

If so, that is where I screwed up, because I (incorrectly) thought DogT: Clone meant:

forall struct S,
  impl DogT for S implies S has a clone()

You're right here - except you missed that Clone either requires Sized or implies this by having no ?Sized methods, the ones which would work without knowing Self's size at the spot

If you want clonable trait objects, you'll need something like

trait CloneDyn {
    fn clone_dyn(&self) -> Box<dyn CloneDyn>;
}
1 Like

The last missing piece, I think, is that rust has a requirement dyn DogT: DogT. All methods usable on T: DogT must be usable on dyn DogT unless they are explicitly excluded with where Self: Sized. So if DogT: SomeTrait, then dyn DogT: SomeTrait as well.

Since dyn DogT: Clone cannot hold, it follows that either DogT: Clone must be removed, or dyn DogT must never exist.

1 Like

@target_san , @daboross

I think I see the problem from Rustc's perspective now. Thanks for the detailed explainations!

1 Like

I was about to ask why we can't just add where Self: Sized, but I realized midway:
If trait B requires where Self: Sized + Clone, then it implies that B is going to, at some point, use self.clone(), which in context of trait B returns a dyn B, while to anyone else a struct C which impl B for C will return a C which is Sized, so even if we were to add a Sized restraint to Self, we wouldn't be able to do anything with it, because either way Self is !Sized in the eyes of a trait object because there may be different types that impl B for MyStruct and not all of them will have the same size
(Sorry if it's convoluted, I might've gotten a bit carried away)

2 Likes

Sometimes I wish we had things like

trait X
where
    if Self: Sized { Self: Clone }
{

}

But I know that would make the type system much more complicated, and make ensuring backwards compatibility much harder if it were allowed for things besides Sized (same reason we don't have a where Self: !Trait bound).

1 Like

If you offer recursion + conditionals, someone will probably figure out how to encode whether a Turing Halts as whether a Trait compile checks. :slight_smile:

3 Likes

I hadn't thought about this before, but it seems like there's an interesting concept of "dynamic cloneability" here: i.e., we want to be able to deep-copy objects via dynamic references. The Clonable/CloneDyn trait above seems like a reasonable way to represent it, but the use of Box seems overly restrictive; all that we want is for there to be some way to create a new trait object deep-copied from an existing one.

It seems like representing this in a usefully generic way probably isn't possible without some extra language features, though, such as placement-new and/or some stable way of dealing with unsized types.