I am a Rust beginner and I can’t get the following code to compile. What I want is to store several traits in a vector and each of the traits should also have read-only access to a borrowed variable.
I am guessing I have to use „Lifetime bounds“ - like discussed in this thread - because if I comment out lines 60-68, the code compiles fine.
Can somebody please explain how to use „lifetime bounds“ - if this is the way to solve the problem - or is this not the Rust way to solve the problem? If there is a better way to achieve what I’m trying to do I’m glad to change my approach to the problem.
The code which doesn’t compile is here and on rust-playground.
struct PixelImageSimple<'a> {
pixels: &'a Vec<i32>,
width: i32,
height: i32,
}
trait ImageOperation<'a> {
fn execute_op(&self);
}
struct ImageOperationSharpen<'a> {
val: i32,
bitmapdata: &'a PixelImageSimple<'a>
}
impl<'a> ImageOperation<'a> for ImageOperationSharpen<'a> {
fn execute_op(&self) {
println!("ImageOperationSharpen - val = {}, width = {}, height = {}, pixels = {:?}",
&self.val, &self.bitmapdata.width, &self.bitmapdata.height,&self.bitmapdata.pixels);
}
}
struct ImageOperationRotate<'a> {
angle: f64,
bitmapdata: &'a PixelImageSimple<'a>
}
impl<'a> ImageOperation<'a> for ImageOperationRotate<'a> {
fn execute_op(&self) {
println!("ImageOperationRotate - angle = {}, width = {}, height = {}, pixels = {:?}",
&self.angle, &self.bitmapdata.width, &self.bitmapdata.height,&self.bitmapdata.pixels);
}
}
struct Image<'a> {
image_operations: Vec<Box<ImageOperation<'a>>>
}
impl<'a> Image<'a> {
fn new() -> Image<'a> {
Image { image_operations: vec![] }
}
fn add_op(&mut self, image_ops: Box<ImageOperation<'a>>) {
self.image_operations.push(image_ops);
}
}
fn main () {
let bitmapdata = vec![1,2,3];
let bitmap = PixelImageSimple { pixels: &bitmapdata, width: 222, height:334 };
let sharpen = ImageOperationSharpen { val: 34, bitmapdata: &bitmap };
let rotate = ImageOperationRotate { angle: 13.32, bitmapdata: &bitmap };
let box_sharpen = Box::new(sharpen);
let box_rotate = Box::new(rotate);
let mut image = Image::new();
image.add_op(box_sharpen);
image.add_op(box_rotate);
println!("execute_op()");
for imageops in image.image_operations.iter() {
imageops.execute_op();
}
}
I get 3 errors for variable 'bitmapdata' and 'bitmap' twice. As I mentioned above: the code compiles fine without lines 60-68 but results in a compiler error with those lines.
Interesting thing: the compilers hint message note:
reference must be valid for the static lifetime...
So the compiler wants a static lifetime? (replacing 'a with 'static in the code didn't help)
lifetime_bounds.rs:52:46: 52:56 error: `bitmapdata` does not live long enough
lifetime_bounds.rs:52 let bitmap = PixelImageSimple { pixels: &bitmapdata, width: 222, height:334 };
^~~~~~~~~~
note: reference must be valid for the static lifetime...
lifetime_bounds.rs:50:34: 69:2 note: ...but borrowed value is only valid for the block suffix following statement 0 at 50:33
lifetime_bounds.rs:50 let bitmapdata = vec![1,2,3];
lifetime_bounds.rs:51
lifetime_bounds.rs:52 let bitmap = PixelImageSimple { pixels: &bitmapdata, width: 222, height:334 };
lifetime_bounds.rs:53
lifetime_bounds.rs:54 let sharpen = ImageOperationSharpen { val: 34, bitmapdata: &bitmap };
lifetime_bounds.rs:55 let rotate = ImageOperationRotate { angle: 13.32, bitmapdata: &bitmap };
...
As an alternative approach I tried a solution using a collection
type CollectionOfImageOperations<'a> = Vec<&'a (ImageOperation<'a> + 'a)>;
but this gave me compile errors which made less sense to me than in the approach above. (It seems like I can only push one trait object to the vector - but why) - see rust-playground for the code and error.
Any hints & tips are welcome and appreciated.
You've run afoul of lifetime elision. When in doubt, write it out!
Note: the comments are because what I'm adding here is not valid Rust syntax. Also note that what follows is not entirely accurate, to avoid being bogged down in details.
fn main () {
/* Lifetimes come from storage, so we'll annotate relevant
variables with lifetimes. We're not defining lifetimes,
we're just assigning names to the lifetimes that the compiler
will work out during borrow checking. */
/* 'a: */ let bitmapdata = vec![1,2,3];
/* We'll also substitute the lifetimes into the types of the
variables and expressions we talk about. Again, you normally
couldn't do this, because you can't name lifetimes *within*
a function. */
/* 'b: */ let bitmap/*: PixelImageSimple<'a> */
= PixelImageSimple {
pixels: &/*'a*/bitmapdata,
width: 222,
height: 334
};
/* We have to pick *one* lifetime here, so we'll pick the
"narrowest" lifetime. We'll cheat here and "assume" that
'a: 'b (read: "'a outlives 'b"); or, in other words, that
'b < 'a (read: "'b is no longer than 'a"). */
let sharpen/*: ImageOperationSharpen<'b> as 'b < 'a */
= ImageOperationSharpen {
val: 34,
bitmapdata: &/*'b*/bitmap/*: PixelImageSimple<'a>*/
};
let box_sharpen/*: Box<ImageOperationSharpen<'b>>*/
= Box::new(sharpen);
/* We'll introduce `'x` here, because it's not immediately clear
what this lifetime should be. The compiler will infer it
from whatever constraints it's been placed under for us. */
/* 'c: */ let mut image/*: Image<'x>*/
= Image::new();
/* Wait, where did `'y` come from? Lifetime elision. When
you're dealing with trait objects, the compiler *must* know
for how long said object is valid. Normally, the compiler
would just look at a type's lifetime parameters, but a trait
object *throws that information away*. As a result, it
needs to preserve this information external to the trait.
This is done using the `Trait + 'k` syntax, where `'k` is
a lifetime that bounds *all* possible implementations of
the trait *in this position*.
This statement is implicit in the original code, but I'm
adding it here to make things explicit. I've also included
the `impl` to denote how the lifetimes transfer around during
the cast. */
let box_sharpen/*: Box<ImageOperation<'b> + 'y>*/
/*where impl<'l> ImageOperation<'l> for ImageOperationSharpen<'l>*/
= box_sharpen/*: Box<ImageOperationRotate<'b>>*/
as Box<ImageOperation/*<'b> + 'y*/>;
/* The only change here is that I *explicitly* borrow `image`
in order to make all the lifetimes involved explicit. In
addition, we now have all the information necessary to work
out what the inferred lifetimes above should be. */
(&/*'c */mut image).add_op(
box_sharpen/* as Box<ImageOperation<'b> + 'y>*/
);
/*where impl<'l> Image::<'l>::add_op<'m>(&'m mut self,
image_ops: Box<ImageOperation<'l> + 'z>)*/
/*implies 'l = 'b, 'm = 'c, 'z = 'y, 'x = 'l = 'b*/
}
All the lifetimes check out... except for 'z = 'y
.
What is 'z
? Whatever it is, it determines the minimum
lifetime of all values that implement ImageOperation
.
What's reasonable? You're talking about a Box
of something,
so what would make sense? The narrowest possible lifetime, or
the widest? The narrowest would render Box<Trait>
almost
unusable, so it must be the widest. The widest is 'static
,
therefore 'z = 'static
.
But wait... if you have Box<ImageOperation<'q> + 'static>
, the
type that implements ImageOperation<'q>
must live at least
as long as the 'static
lifetime... which means that 'q
must
also be 'static
.
By this reasoning, 'x = 'l = 'b = 'static
. But this implies
that when we initialise sharpen
, we do so with the following
expression:
bitmapdata: &'static bitmap: PixelImageSimple<'a>
But that can't be right; you can't have a reference to something
that outlives the something being referenced. That means we
require 'a
to outlive 'static
... which means 'a
is
also 'static
.
But 'a
is a stack frame; it cannot be 'static
!
Thus, the program is not sound.
... so what if we just explicitly tell the compiler that we
don't want a + 'static
bound on our trait objects?
struct Image<'a> {
image_operations: Vec<Box<ImageOperation<'a> + 'a>>
}
impl<'a> Image<'a> {
fn new() -> Image<'a> {
Image { image_operations: vec![] }
}
fn add_op(&mut self, image_ops: Box<ImageOperation<'a> + 'a>) {
self.image_operations.push(image_ops);
}
}
// ...
fn main() {
// ...
(&/*'c */mut image).add_op(
box_sharpen/* as Box<ImageOperation<'b> + 'y>*/
);
/*where impl<'l> Image::<'l>::add_op<'m>(&'m mut self,
image_ops: Box<ImageOperation<'l> + 'z>)*/
/*implies 'l = 'b, 'm = 'c, 'z = 'y, 'x = 'l = 'b*/
}
And it now compiles.
Addendum (suggested by aatch): additionally, the lifetime on
ImageOperation
itself seems misplaced. You aren't using it for anything, and it isn't necessary for the code to work. In that case, you end up dealing withBox<ImageOperation + 'a>
s, which is an even better demonstration of why you need trait object lifetime bounds.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With