Why does this closure no longer compile if I remove the braces around its body?

Hello rustaceans,

I'm storing owned trait objects in a HashMap, and I'm trying to borrow one of them as a mutable reference.
The following code does compile.

trait Data {}

struct Registry {
    map: HashMap<String, Box<dyn Data>>,
}

impl Registry {
    fn get_mut(&mut self, key: &str) -> Option<&mut dyn Data> {
        self.map.get_mut(key).map(|b| { b.deref_mut() })
    }
}

However, try to remove the braces and the code no longer compiles (link to playground).

fn get_mut(&mut self, key: &str) -> Option<&mut dyn Data> {
    self.map.get_mut(key).map(|b| b.deref_mut())  // no braces
}
error: lifetime may not live long enough
  --> src/main.rs:12:39
   |
11 |     fn get_mut(&mut self, key: &str) -> Option<&mut dyn Data> {
   |                - let's call the lifetime of this reference `'1`
12 |         self.map.get_mut(key).map(|b| b.deref_mut())
   |                                       ^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'static`
   |
help: to declare that the trait object captures data from argument `self`, you can add an explicit `'_` lifetime bound
   |
11 |     fn get_mut(&mut self, key: &str) -> Option<&mut dyn Data + '_> {
   |                                                              ++++
help: consider adding 'move' keyword before the nested closure
   |
12 |         self.map.get_mut(key).map(move |b| b.deref_mut())
   |                                   ++++

The two suggestions don't seem to work. But the braceless version does compile if I replace the trait by a struct Data, or if I use a match instead of map. Interestingly, rustfmt automatically removes the braces from the closure, which make the compilation fail :no_mouth:

I'm quite surprised by what's happening here... What is different about the trait case?
Is |x| res not the same as |x| {res}?

2 Likes

First of all, this does seem like unintended compiler behavior. Especially the rustfmt thing, also I could find a compiler suggestion if I included an explicit return to the closure

impl Registry {
    fn get_mut(&mut self, key: &str) -> Option<&mut dyn Data> {
        self.map.get_mut(key).map(move |b| return {&mut **b})
    }
}
warning: unnecessary braces around `return` value
  --> src/main.rs:12:51
   |
12 |         self.map.get_mut(key).map(move |b| return {&mut **b})
   |                                                   ^        ^
   |
   = note: `#[warn(unused_braces)]` on by default
help: remove these braces
   |
12 -         self.map.get_mut(key).map(move |b| return {&mut **b})
12 +         self.map.get_mut(key).map(move |b| return &mut **b)

where following this suggestion also breaks the code.


By the way, I’ve decided to re-write b.deref_mut with the more idiomatic &mut **b, since that doesn’t change anything about the behavior, anyways.


As for why the complication only occurs with the trait, not a struct Data, that’s easy: The complication here, i.e. what the compiler gets “confused” with, is the lifetime of the trait object. See more here on trait object lifetimes.

I.e. the code

struct Registry {
    map: HashMap<String, Box<dyn Data>>,
}

impl Registry {
    fn get_mut(&mut self, key: &str) -> Option<&mut dyn Data> {
        …
    }
}

if all lifetimes are un-elided becomes

struct Registry {
    map: HashMap<String, Box<dyn Data + 'static>>,
}

impl Registry {
    fn get_mut<'a, 'b>(self: &'a mut Self, key: &'b str) -> Option<&'a mut (dyn Data + 'a)> {
        …
    }
}

Then the types dyn Data + 'static and dyn Data + 'a are not exactly the same anymore, however the 'static one can be coerced to the + 'a version (via unsizing coercion[1]).

It seems to be this very unsizing coercion that the compiler trips up with in one of the versions. Coercion sites become relevant, as listed here, but the behavior doesn’t quite match what’s documented there, either. It does list “the final line of a block” as a coercion site, so I still think it may be related, but it also lists “ any expression in a return statement”, and my example with return above seems to contradict it.


Since this is a bug – at the very least a bug with suggestion diagnostics, and rustfmt, but probably the code without braces should be made to compile, too, if there isn’t any strong reason against such a change – someone should open an issue on GitHub about this, if it doesn’t exist already.


  1. notably this conversion is not from variance&subtyping, as it’s behind a mutable reference ↩︎

7 Likes

@steffahn explained why there's an error in detail, so I'll just note an existing issue:

7 Likes

Thank you for the detailed explanation!
I've added some info to the GitHub issue. In the meantime, I'll insert a cast as a workaround, so that it survives rustfmt:

self.map.get_mut(key).map(|b| b.deref_mut() as _) // works
2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.