Understanding trait composition and Box


#1

So I had this discussion on the IRC Channel with a few people, but I was in a hurry and did not manage to wrap my hand around it. The problem, briefly speaking is I have:

trait Operator {//... }

struct Node {
op: Box<Operator> 
}

However I want to be able to copy the op. For this I tried Operator: Clone, which however does not work as the compiler says

^^^^^^^^^^^^^^^^^ the trait `Operator` cannot be made into an object

So first I want to check that I understand what trait X : Y + Z means. My understanding of this is saying whatever type T implements X that type must implement also Y and Z. Secondly, I’m not sure I totally understand why I can’t have my trait to require that the underlying object is cloneable? Additionally, if I do need this what is the idiomatic way to achieve it? I can have a specific method in the Operator trait which constructs the new type inside a Box and returns it (e.g. the concrete type clones itself and constructs a new Box), however is not that what Operator: Clone was suppose to do in the first place?

A short example: https://is.gd/mbiBQl


#2

You’re trying to use Operator as a trait object and Copy is not object-safe, see here:

https://doc.rust-lang.org/book/trait-objects.html

(Bottom of the page)


#3

That still does not answer me why semantically it is not allowed (I understand the rules, but why are their there). For example here is how I can sort of simulate it, requiring that any type of Operator can be cloned and having a clone of the box: https://is.gd/rVOq5I. Why is this not automatically happening if you have Box<Clone>?


#4

The rule exists because all values need a place to be stored, and the compiler needs to know how big that value is in order to know how much space it will take up. Think of code like this

trait Trait;

#[derive(Clone)]
struct Small {
    x: [u8; 1]
}

#[derive(Clone)]
struct Big {
    x: [u8; 1000000]
}

impl Trait for Small {}
impl Trait for Big {}

fn do_stuff(val: Box<Trait + Clone>) {
    let mut thing = val.clone();
    // do stuff to thing
}

do_stuff needs to allocate memory on the stack to store the new struct. But it can’t tell how much to allocate, since val.clone() could return either a 1-byte value or a 1000000-byte value.


#5

But aren’t Box a fat pointer which stores the pointer only on the stack, but the object can be in stored in the heap or whatever has allocated the actual object. Even in your example I can unbox the trait to a concrete type clone in on the heap and create a Box out of that value? (I guess one of my understanding here is wrong, could you point me out which?)


#6

What you want to achieve is cloning from Box<Operator> to Box<Operator>.
Operator + Clone gives you cloning from Op to Op where Op: Operator. Now, putting it into trait object – Box<Operator> – would give you cloning from Box<Operator> to Self. But what type is Self? It may have different type for each object – the problem is that this Self isn’t known at compile time. Even if you want to immediately box it and cast to a trait object, you still need the caller to know the exact size (to copy the data) and type (to fill the vtables) of the object.

So the only solution to that problem is to make the callee box and virtualize the object on its own (the callee knows the precise type of the object, so it can do it), just as you do with your clone_op trait method. The problem is that it can’t be done generically for any trait. It would require something like being able to abstract over traits, something like that:

trait Clone {
    fn clone(&self) -> Self where Self: Sized;

    fn clone_and_box<Trait: trait>(&self) -> Box<Trait> where Self: Sized + Trait;
}

The only thing I can think of to make the life easier in this case is to add the following impl:

impl Clone for Box<Operator> {
    fn clone(&self) -> Box<Operator> {
        self.clone_op()
    }
}

That allows you to just derive Clone on NodeData.

Unfortunately, the clone_op has to be implemented manually, even if it’s the same code for each struct. It can’t be made a default, since that would require Self: Clone, and that makes trait objects impossible. If only it would be possible to do something like Self: Sized → Clone, maybe this might work.