Confused about deref_mut()


#1

Consider the following program:

use std::ops::DerefMut;

struct St;

trait Tr { }

impl Tr for St { }

fn make_it() -> Box<Tr> {
    Box::new(St)
}

fn take_it(_: &mut Tr) {
}

fn main() {
    let mut oracle = make_it();
    //take_it(&mut *oracle);       // WORKS
    //take_it(oracle.deref_mut()); // ERROR
}

I don’t understand why the first invocation of take_it() works, while the second one yields a compiler error. The error is:

src/main.rs:19:13: 19:19 error: `oracle` does not live long enough
src/main.rs:19     take_it(oracle.deref_mut()); // ERROR
                           ^~~~~~
src/main.rs:16:11: 20:2 note: reference must be valid for the block at 16:10...
src/main.rs:16 fn main() {
src/main.rs:17     let mut oracle = make_it();
src/main.rs:18     //take_it(&mut *oracle);       // WORKS
src/main.rs:19     take_it(oracle.deref_mut()); // ERROR
src/main.rs:20 }
src/main.rs:17:32: 20:2 note: ...but borrowed value is only valid for the block suffix following statement 0 at 17:31
src/main.rs:17     let mut oracle = make_it();
src/main.rs:18     //take_it(&mut *oracle);       // WORKS
src/main.rs:19     take_it(oracle.deref_mut()); // ERROR
src/main.rs:20 }
error: aborting due to previous error

I would have expected the two lines to be pretty much equivalent – the second being a desugaring of the first.

Rust 1.8.0.


#2

Strange that it’s trying to require a lifetime for the whole function block. I can’t figure out why either.

It works if you bind the reference to a local:

let r = oracle.deref_mut();
take_it(r);

#3

Something similar was reported as a bug by @kirillkh earlier this month:


#4

I suspect this has something to do with the different default lifetime bounds for Box<Trait> versus &Trait. If I change the signature of take_it to this, it works:

fn take_it(_: &mut (Tr + 'static)) {}

#5

More generally, this works:

use std::ops::DerefMut;

struct St;

trait Tr { }

impl Tr for St { }

fn make_it() -> Box<Tr> {
    Box::new(St)
}

fn take_it<'a>(_: &mut (Tr + 'a)) {
}

fn main() {
    let mut oracle = make_it();
    take_it(&mut *oracle);       // WORKS
    take_it(oracle.deref_mut()); // WORKS
}

The problem is the default bound in the function argument is &'a mut (Tr + 'a), which is too restrictive, because in this case, the lifetime bound on Tr is invariant due to mutability, and so the reference is being forced to have a 'static liftime. Also, that bound is basically useless, because you’ll never get the borrow back.


#6

Very interesting, thanks. Let me try and dig into the details of your answer to see how far I understand.

the default bound in the function argument is &'a mut (Tr + 'a)

I can see that this is true as per RFC 599. I’m afraid I don’t quite understand what this means though. The first occurrence of 'a refers to the lifetime of the thing referred to by the mutable reference. The second occurrence of 'a refers to the lifetime of… what exactly? Of the trait object itself (which would be the same as the first 'a occurrence); or of the lifetime of all references “inside” the trait object, as suggested by the Rust By Example chapter on lifetime bounds? —It says:

T: Trait + 'a: Type T must implement trait Trait and all references in T must outlive 'a.

Now for the next part of your answer:

the lifetime bound on Tr is invariant due to mutability, and so the reference is being forced to have a 'static liftime

The Rustonomicon explains:

&'a mut T is variant over 'a but invariant over T

So in our case, this is saying that even though 'static: 'a, still &'a mut (Tr + 'a) is not a subtype of &'a mut (Tr + 'static).

But the latter is in fact the type of oracle.deref_mut(), i.e. the trait object in our case has 'static lifetime. This is because the return type of make_it(), Box<Tr>, desugars to Box<Tr+'static>, again per RFC 599. Okay.

Next, I’ll try to understand why your fix works. Your new signature for take_it() I think desugars to:

fn take_it<'a, 'b>(_: &'b mut (Tr + 'a))

I can confirm that this too fixes it. The reason this works is that 'a now takes the value 'static without forcing the same on 'b.

Finally, I’d still like to understand why

take_it(&mut *oracle);

works even with the original version of take_it(). It seems like the type of &mut *oracle must be &'a mut (Tr + 'b) with 'b being something other than 'static… but I don’t understand why that would be.

Interesting journey into the dark heart of Rust so far :slight_smile: