How to unbox elements contained in polymorphic vectors?

3.8k views Asked by At

After reading this answer to "Vector of objects belonging to a trait", it looks like Rust does automatic unboxing. Is this the case?

My code doesn't compile and I don't understand how that answer's code could compile.

What is the correct way to unbox the elements of a polymorphic vector, one containing boxed traits?

I've read Rust by Example and the Box documentation and I can't see any method that looks like unbox().

My code is:

trait HasArea {
    fn area(&self) -> f64;
}

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl HasArea for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

struct Square {
    x: f64,
    y: f64,
    side: f64,
}

impl HasArea for Square {
    fn area(&self) -> f64 {
        self.side * self.side
    }
}

fn print_area<T: HasArea>(shape: T) {
    println!("This shape has an area of {}", shape.area());
}

fn main() {
    let c = Circle {
        x: 0.0f64,
        y: 0.0f64,
        radius: 1.0f64,
    };

    let s = Square {
        x: 0.0f64,
        y: 0.0f64,
        side: 1.0f64,
    };

    print_area(c);
    print_area(s);

    let vec: Vec<Box<HasArea>> = Vec::new();
    vec.push(Box::new(c));
    vec.push(Box::new(s));

    for x in vec {
        print_area(x)
    }
}

My error is:

   Compiling rustgraph v0.1.0 (file:///home/chris/lunch/rustgraph)
error[E0277]: the trait bound `Box<HasArea>: HasArea` is not satisfied
  --> src/main.rs:54:9
   |
54 |         print_area(x)
   |         ^^^^^^^^^^ the trait `HasArea` is not implemented for `Box<HasArea>`
   |
   = note: required by `print_area`
4

There are 4 answers

2
sheikh_anton On BEST ANSWER

You can dereference it like print_area(*x), but it won't work for other reasons: the Sized bound for the print_area argument. Your function needs to know the size of its arguments.

You have other problems in your code: you are trying to push into an immutable vector and you are trying to box moved values. These were moved after you used it in print_area().

My opinion is that it would be easier to make print_area a method which takes an immutable reference. This will work as you expected.

trait HasArea {
    fn area(&self) -> f64;
    fn print_area(&self) {
        println!("This shape has area of {}", self.area());
    }
}

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl HasArea for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

struct Square {
    x: f64,
    y: f64,
    side: f64,
}

impl HasArea for Square {
    fn area(&self) -> f64 {
        self.side * self.side
    }
}

fn print_area<T: HasArea>(shape: &T) {
    println!("This shape has an area of {}", shape.area());
}

fn main() {
    let c = Circle {
        x: 0.0f64,
        y: 0.0f64,
        radius: 1.0f64,
    };

    let s = Square {
        x: 0.0f64,
        y: 0.0f64,
        side: 1.0f64,
    };

    c.print_area();
    s.print_area();

    let mut vec: Vec<Box<HasArea>> = Vec::new();
    vec.push(Box::new(c));
    vec.push(Box::new(s));

    for x in vec {
        x.print_area();
    }
}
0
E_net4 On

After reading https://stackoverflow.com/a/25819164/129805 it looks like Rust does automatic unboxing. Is this the case?

Not as automatic as you might think. In fact, you were looking for an unbox method while Box<T> implements Deref for target T. That means you should either call as_ref() or rely on Deref coercion. Note that a T is not possible for unsized types, and since you are relying on polymorphic types, the consumer function will have to accept a reference.

I took the liberty of fixing main and print_area to make it work. The vector was also improperly declared as immutable.

fn print_area<T: HasArea + ?Sized>(shape: &T) {
    println!("This shape has an area of {}", shape.area());
}

fn main() {
    let c = Circle {
        x: 0.0f64,
        y: 0.0f64,
        radius: 1.0f64,
    };

    let s = Square {
        x: 0.0f64,
        y: 0.0f64,
        side: 1.0f64,
    };

    print_area(&c);
    print_area(&s);

    let mut vec: Vec<Box<HasArea>> = Vec::new();
    vec.push(Box::new(c));
    vec.push(Box::new(s));

    for x in vec {
        print_area(&*x)
    }
}
3
ljedrz On

As an alternative to what E_net4 suggested, instead of boxing your traits you could use a Vec with references to make it work:

fn print_area<T: HasArea+?Sized>(shape: &T) {
    println!("This shape has an area of {}", shape.area());
}

let mut vec: Vec<&HasArea> = Vec::new();
vec.push(&c);
vec.push(&s);

for x in vec {
    print_area(x)
}
0
Shepmaster On

To answer your direct question:

How to unbox elements contained in polymorphic vectors?

You cannot. Once something has been boxed and had the concrete type erased, that's it. A Box<SomeTrait> cannot be made back into a SomeConcreteType, because nothing knows what that concrete type is.


To solve the problem in the code... check the error message again:

the trait bound Box<HasArea>: HasArea is not satisfied

That's because a reference to a trait (or a box of a trait) does not implement that trait!

To allow your program to compile and run as you originally wrote it, you only need to implement the trait for boxes, and we might as well do references too:

impl<T: ?Sized> HasArea for Box<T>
    where T: HasArea
{
    fn area(&self) -> f64 { (**self).area() }    
}

impl<'a, T: ?Sized> HasArea for &'a T
    where T: HasArea
{
    fn area(&self) -> f64 { (**self).area() }    
}

This allows your fixed-up main to run:

fn main() {
    let c = Circle {
        x: 0.0f64,
        y: 0.0f64,
        radius: 1.0f64,
    };

    let s = Square {
        x: 0.0f64,
        y: 0.0f64,
        side: 1.0f64,
    };

    print_area(&c);
    print_area(&s);

    let vec: Vec<Box<HasArea>> = vec![Box::new(c), Box::new(s)];

    for x in vec {
        print_area(x)
    }
}

Here, we pass a reference of c and s to print_area, to avoid transferring ownership. We also use the vec! macro to construct the vector with much less ceremony.