fn works<'a>(foo: &Option<&'a mut String>, s: &'a mut String) {}
fn error<'a>(foo: &RefCell<Option<&'a mut String>>, s: &'a mut String) {}
let mut s = "hi".to_string();
let foo = None;
works(&foo, &mut s);
// with this, it errors
// let bar = RefCell::new(None);
// error(&bar, &mut s);
s.len();
If I put in the two lines with the comment, the following error occurs:
error[E0502]: cannot borrow `s` as immutable because it is also borrowed as mutable
--> <anon>:16:5
|
14 | error(&bar, &mut s);
| - mutable borrow occurs here
15 |
16 | s.len();
| ^ immutable borrow occurs here
17 | }
| - mutable borrow ends here
The signatures of works()
and errors()
look fairly similar. But apparently the compiler knows that you can cheat on it with a RefCell
, because the borrow checker behaves differently.
I can even "hide" the RefCell
in another type of my own, but the compiler still always does the right thing (errors in case a RefCell
could be used). How does the compiler know all that stuff and how does it work? Does the compiler mark types as "interior mutability container" or something like that?
RefCell<T>
contains anUnsafeCell<T>
which is a special lang item. It isUnsafeCell
that causes the error. You could check with:But the error is not due to compiler recognizing an UnsafeCell introduces interior mutability, but that an
UnsafeCell
is invariant in T. In fact, we could reproduce the error using PhantomData:or even just anything that is contravariant or invariant in the lifetime
'a
:The reason you can't hide a RefCell is because variance is derived through the fields of the structure. Once you used
RefCell<T>
somewhere, no matter how deep, the compiler will figure outT
is invariant.Now let's see how the compiler determine the E0502 error. First, it's important to remember that the compiler has to choose two specific lifetimes here: the lifetime in the type of the expression
&mut s
('a
) and the lifetime in the type ofbar
(let's call it'x
). Both are restricted: the former lifetime'a
has to be shorter than the scope ofs
, otherwise we would end up with a reference living longer than the original string.'x
has to be larger than the scope ofbar
, otherwise we could access an dangling pointer throughbar
(if a type has a lifetime parameter the compiler assume the type can access a value with that lifetime).With these two basic restriction, the compiler goes through the following steps:
bar
isContravariant<&'x i32>
.error
function accepts any subtype ofContravariant<&'a i32>
, where'a
is the lifetime of that&mut s
expression.bar
should be a subtype ofContravariant<&'a i32>
Contravariant<T>
is contravariant overT
, i.e. ifU <: T
, thenContravariant<T> <: Contravariant<U>
.&'x i32
is a supertype of&'a i32
.'x
should be shorter than'a
, i.e.'a
should outlive'x
.Similarly, for an invariant type, the derived relation is
'a == 'x
, and for convariant,'x
outlives'a
.Now, the problem here is that the lifetime in the type of
bar
lives until the end of scope (as per restriction mentioned above):In both contravariant and invariant cases,
'a
outlives (or equals to)'x
means the statements.len()
must be included in the range, causing borrowck error.Only in the covariant case we could make the range of
'a
shorter than'x
, allowing the temporary object&mut s
be dropped befores.len()
is called (meaning: ats.len()
,s
is not considered borrowed anymore):