Prolonging Temporaries in Rust

A colleague of mine learning Rust had an interesting type / borrow checker error. The solution needs a less-used feature of Rust (which basically exists precisely for this kind of thing), so I thought I’d document it.

The code was like this:

let maybe_foo = if some_condition {
    thing.get_ref() // returns Option, borrowed from `thing`
} else {
    thing.get_owned() // returns Option
};

use(maybe_foo);

If you want to follow along, here is a full program that does this ( playpen
):

#[derive(Debug)]
struct Foo;

struct Thingy {
    foo: Foo
}

impl Thingy {
    pub fn get_ref(&self) -> Option {
        Some(&self.foo)
    }
    pub fn get_owned(&self) -> Option {
        Some(Foo)
    }
    pub fn new() -> Self {
        Thingy {
            foo: Foo
        }
    }
}



pub fn main() {
    let some_condition = true;
    let thing = Thingy::new();

    let maybe_foo = if some_condition {
        thing.get_ref() // returns Option, borrowed from `thing`
    } else {
        thing.get_owned() // returns Option
    };

    println!("{:?}", maybe_foo);
}

I’m only going to be changing the contents of main()
here.

What’s happening here is that a non- Copy
type, Foo
, is returned in an Option
. In one case, we have a reference to the Foo
, and in another case an owned copy.

We want to set a variable to these, but of course we can’t because they’re different types.

In one case, we have an owned Foo
, and we can usually obtain a borrow from an owned type. For Option
, there’s a convenience method .as_ref()
that does this. Let’s try using that ( playpen
):

let maybe_foo = if some_condition {
    thing.get_ref()
} else {
    thing.get_owned().as_ref()
};

This will give us an error.

error: borrowed value does not live long enough
  --> :32:5
   |
31 |         thing.get_owned().as_ref()
   |         ----------------- temporary value created here
32 |     };
   |     ^ temporary value dropped here while still borrowed
...
35 | }
   | - temporary value needs to live until here

error: aborting due to previous error

The problem is, thing.get_owned()
returns an owned value. There’s nothing that it gets anchored to (we don’t set its value to a variable), so it is just a temporary – we can call methods on it, but once we’re done the value will go out of scope.

What we want is something like

let maybe_foo = if some_condition {
    thing.get_ref()
} else {
    let owned = thing.get_owned();
    owned.as_ref()
};

but this will still give a borrow error – owned
will still go out of scope within the if
block, and we need the reference to it last as long as maybe_foo
(outside the block) is supposed to last.

So this is no good.

An alternate solution here can
be copying/cloning the Foo
in the first
case by calling .map(|x| x.clone())
or .cloned()
or something. Sometimes you don’t want to clone, so this isn’t great.

Another solution here – the generic advice for dealing with values which may be owned or borrow – is to use Cow
. It does incur a runtime check, though; one which can be optimized out if things are inlined enough.

What we need to do here is to extend the lifetime of the temporary returned by thing.get_owned()
. We need to extend it past
the scope of the if
.

One way to do this is to have an Option
outside that scope which we mutate ( playpen
).

let mut owned = None;
let maybe_foo = if some_condition {
    thing.get_ref()
} else {
    owned = thing.get_owned();
    owned.as_ref()
};

This works in this case, but in this case we already had an Option
. If get_ref()
and get_owned()
returned &Foo
and Foo
respectively, then we’d need to do something like:

let mut owned = None;
let maybe_foo = if some_condition {
    thing.get_ref()
} else {
    owned = Some(thing.get_owned());
    owned.as_ref().unwrap()
};

which is icky since it introduces an unwrap.

What we really need is a way to signal to the compiler that it needs to hold on to that temporary for the scope of the enclosing block.

We can do that! ( playpen
)

let owned; // :hushed::hushed::hushed::hushed::hushed:
let maybe_foo = if some_condition {
    thing.get_ref()
} else {
    owned = thing.get_owned();
    owned.as_ref()
};

We know that Rust doesn’t do “uninitialized” variables. If you want to name a variable, you have to initialize it. let foo;
feels rather like magic in this context, because it looks like we’ve declared an uninitialized variable.

What’s less well known is that Rust can
do “deferred” initialization. Here, you declare a variable and can initialize it later, but expressions involving the variable can only exist in branches where the compiler knows it has been initialized.

This is the case here. We declared the owned
variable beforehand. It now lives in the outer scope and won’t be destroyed until the end of the outer scope. However, the variable cannot be used directly in an expression in the first branch, or after the if
. Doing so will give a compile time error saying use of possibly uninitialized variable: `owned`
. We can only use it in the else
branch because the compiler can see that it is unconditionally initialized in that branch.

We can still read the value of owned
indirectly through maybe_foo
from outside the branch. This is okay because the storage of owned
is guaranteed to live as long as the outer scope, and maybe_foo
borrows from it. The only time maybe_foo
is set to a value inside owned
is when owned
has been initialized, so it is safe.

稿源:In Pursuit of Laziness (源链) | 关于 | 阅读提示

本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 综合编程 » Prolonging Temporaries in Rust

喜欢 (0)or分享给?

专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录