Rust lifetimes: from 'static
to ecstatic.
Foo
Case study: the pick()
function
In these series we'll be dealing with kind of the lifetime-equivalent of the trolley problem: fn pick(&str, &str) -> &str
and friends.
The body will always be along the lines of:
#![allow(unused)] fn main() { fn pick(left: &'_?? str, right: &'_?? str) -> &'_?? str { if ::rand::random() { left } else { right } } }
And the whole question will be about what should we put instead of each '_??
.
Let's try elision?
For starters, let's try the typical works-way-more-often-than-it-deserves do-no-overcomplicate approach of using elided lifetimes everywhere:
fn pick(left: &'_ str, right: &'_ str)
-> &'_ str
❌ this fails because I wanted to make your life miserable the lifetime elision rules do not handle this situation: the inputs are so symmetric there is no clear "borrowee" argument.
Let's try repeating
Now we could also try using distinct lifetime parameters for all three, but that will "never" work: we return a borrowing thing, so it has to be borrowing from at least one of the arguments, thus, in practice, a borrowing return type almost always has to involve a lifetime parameter that appears among one of the input types.
Given how symmetric our code is, the sensible thing to do now would be to repeat all the lifetimes:
fn pick<'ret>(s1: &'ret str, s2: &'ret str)
-> &'ret str
And this does:
- compile ✅
- not result in unnecesarily restrictive API ✅
Yay! 🥳
-
Aside: beginners often forget about the latter. Massaging a signature so that it becomes compatible with a given implementation often means we are making that function less compatible with certain call-site situations. While it can often be an acceptable loss, or a necessary one, other times that initial
cargo check
passing can hide that the API has become near-uncallable.That's why it is advisable to do this massaging of a function signature dance with an example usage / unit test next to it, so as to see the "two sides" of the caller/callee situation.
Congratulations, puzzle solved. Now to the next puzzle!
Consider:
use ::std::sync::{Arc, Mutex};
#[derive(Clone)]
struct Person<'name> {
name: Arc<Mutex<&'name str>>,
}
fn pick(p1: Person<'_??>, p2: Person<'_??>)
-> &'_?? str
{
if ::rand::random() {
*p1.name.lock().unwrap()
} else {
*p2.name.lock().unwrap()
}
}
and with the following test case:
#[test]
fn test()
{
let local = String::from("non-static");
let p1: Person<'static> = Person {
name: Arc::new(Mutex::new("static")),
};
let p2: Person<'_> = Person {
name: Arc::new(Mutex::new(&local)),
};
let choice = pick(p1, p2);
dbg!(choice);
}
Now, if you try the 'ret
-everywhere approach, the test function will not compile!
error[E0597]: `local` does not live long enough
--> src/lib.rs:49:35
|
45 | let p1: Person<'static> = Person {
| --------------- type annotation requires that `local` be borrowed for `'static`
...
49 | name: Arc::new(Mutex::new(&local)),
| ^^^^^^ borrowed value does not live long enough
...
53 | }
| - `local` dropped here while still borrowed
From the error message, we can guess that Rust is now convinced that our p2: Person<'_>
is actually a Person<'static>
(at which point it complains about &local
not being able to be borrowed for 'static
/ 'forever
due to the impending doom that looms over the local
variable).
But why is that? Well, remember how the input of our pick()
function was repeating the same 'ret
lifetime parameter for both function arguments. Well, that's exactly what repeating a lifetime parameter means: lifetime equality! If p1
involves a 'static
lifetime, then it means that p2
's to-be-inferred lifetime has to be 'static
. Hence the error.
-
For the skeptical ones who may still think the lifetime of the return value may also be playing a role here (which it is not), and for the otherwise just curious people, here is an interesting thing: since having both function args involving the same lifetime parameter was enough to cause this restriction, we can actually get rid of the return type altogether (and thus the function body as well), and still have the same problem! 😄
fn pick<'ret>(_: Person<'ret>, _: Person<'ret>) // -> no return type whatsoever { // no meaningful body either 🙃 }
Now, at this point we may wonder about the initial &str
case, and see if it also suffers from this problem (had we been overly optimistic?)
fn pick<'ret>(s1: &'ret str, s2: &'ret str)
-> &'ret str
{
"…"
}
#[test]
fn test()
{
let local = String::from("non-static");
let s1: &'static str = "static";
let s2: &'_ str = &local;
let choice = pick(s1, s2);
dbg!(choice);
}
Aaand… it does compile!
So, what has changed? Why/how does &'_ str
work but Person<'_>
not?
And the answer is that the '_
in &'_ str
is "allowed to shrink", which is not the case for Person<'_>
.
VARIANCE yadda yadda
Indeed, the main change here is that Person<'_>
, contrary to &'_ str
, does not allow its lifetime '_
to "shrink"; which is something the 'ret
-everywhere approach was (probably unknowingly) relying on for the signature not to be restrictive. As
#[test]
fn test()
{
let local1 = String::from("Jane");
let local2 = String::from("Dove");
let p1: Person<'_> = Person {
name: Arc::new(Mutex::new(&local1)),
};
let p2: Person<'_> = Person {
name: Arc::new(Mutex::new(&local2)),
};
let choice = pick(p1.clone(), p2.clone());
dbg!(choice);
if ::rand::random() {
drop(p1); drop(local1);
drop(p2); drop(local2);
} else {
drop(p2); drop(local2);
drop(p1); drop(local1);
}
}
Lifetime elision
- Prerequisite: elided lifetimes.
The lifetime elision rules are just there as convenience sugar for what could otherwise be written in a more verbose manner. As with any sugar that does not want to become overly terse and footgunny (cough, C++, cough), it only makes sense to have it in clear unambiguous cases.
Lifetime elision in function bodies
Inside function bodies, that is, in a place where type inference is allowed, any elided lifetimes, implicit or not, will adjust to become whatever suits the code / whatever type-inference (with borrow-checking) dictates.
Lifetime elision in function signatures
By "function signatures" the following rules will apply to:
-
A function (item) signature:
-
fn some_func(…) -> …
-
-
A function pointer type:
-
type MyCb = fn(…) -> …; // ^^^^^^^^^^
-
-
A
Fn{,Mut,Once}
trait bound:-
: Fn{,Mut,Once}(…) -> …
-
impl Fn{,Mut,Once}(…) -> …
-
dyn Fn{,Mut,Once}(…) -> …
-
So, for all these, it turns out there are only two categories of "clear unambiguous cases":
-
When lifetimes play no meaningful role
That is, when the function returns nothing that borrows or depends on inputs. That is, when the return type has no "lifetime" parameters:
#![allow(unused)] fn main() { fn eq(s1: &'_ str, s2: &'_ str) -> bool // <- no "lifetimes"! }
Since there isn't really any room for lifetimes/borrows subtleties here, the unsugaring will be a maximally flexible one. Given that repeating a lifetime parameter name is a restriction (wherein two lifetime-generic types will need to be using equal "lifetimes" / matching regions), we get to be maximaly flexible / lenient / loose by not doing that: by introducing and using distinct lifetime parameters for each lifetime placeholder:
#![allow(unused)] fn main() { fn eq<'s1, 's2>(s1: &'s1 str, s2: &'s2 str) -> bool }
-
With borrowing getters
With thus a clear unambiguous receiver / thing being borrowed:
-
#![allow(unused)] fn main() { fn get(from_thing: &'_ Thing) // same for `BorrowingThing<'_>`. -> &'_ SomeField // replacing `&'_` with `&'_ mut` works too, of course. }
-
In case of single "lifetime" placeholder among all the inputs;
-
⇒ the borrow of
SomeField
necessarily stems from it; -
⇒ the output is to be "connected" to that input borrow by repeating the lifetime parameter.
fn get<'ret> (from_thing: &'ret Thing) -> &'ret SomeField
-
-
#![allow(unused)] fn main() { fn get(&'_ self, key: &'_ str) -> &'_ Value }
-
A
&[mut] self
method receiver, despite the multiple lifetime placeholders among all the inputs, is "favored" and by default deemed to be the "thing being borrowed". -
⇒ the borrow of the
Value
is to be "connected" to theself
receiver; andkey
, on the other hand, is in a "I'm not borrowed" kind of situation. -
⇒
key
will thus be using its own distinct "dummy" lifetime, whereasself
and the returnedValue
will be using a single/repeated lifetime parameter.fn get<'ret, '_key> ( self: &'ret Self, key: &'_key str, ) -> &'ret Value
-
-
Regarding fn
-pointers and Fn…
traits, the same rules apply, it's just that the named "generic" lifetime parameters for these things have to be introduced as "nested generics" / higher-rank lifetime parameters, using the for<>
syntax.
-
In the
fn
-pointer case, we have:let eq: fn(s1: &'_ str, s2: &'_ str) -> bool; // stands for: let eq: for<'s1, 's2> fn(s1: &'s1 str, s2: &'s2 str) -> bool;
-
In the
Fn…
case, we have:impl/dyn Fn…(&'_ str, &'_ str) -> bool> // stands for impl/dyn for<'s1, 's2> Fn…(&'s1 str, &'s2 str) -> bool
So the rule of thumb is that "lifetime holes in ouput connect with an unambiguous lifetime hole in input".
-
Technically, lifetime holes can connect with named lifetimes, but mixing up named lifetimes with elided lifetimes for them to "connect" is asking for trouble/confusing code: it ought to be linted against, and I hope it will end up denied in a future edition:
fn example<'a>( a: impl 'a + AsRef<str>, _debug_info: &'static str, ) -> impl '_ + AsRef<str> { a // Error, `a` does not live for `'static` }
Lifetime elision in dyn Traits
Yep, yet another set of rules distinct from that of elision in function signatures, and this time, for little gain 😔
- tip: if you're the one writing the Rust code, avoid using these to reduce the cognitive burden of having to remember this extra set of rules!
This is a special form of implicit elision: dyn Traits
1. Indeed, behind a dyn
necessarily lurks a "lifetime"/region: that of owned-usability.
dyn Traits // + 'usability?
-
⚠️ this is different than the explicitly elided lifetime,
'_
.That is, if you see a
dyn '_ + Trait
it will be the rules for lifetime elision in function signatures which shall govern what'_
means:fn a(it: Box<dyn Send>) // :impl Fn(Box<dyn 'static + Send>) fn b(it: Box<dyn '_ + Send>) // :impl for<'u> Fn(Box<dyn 'u + Send>)
When I write Traits
, it is expected to cover situations such as Trait1 + Trait2 + … + TraitN
Rules of thumb for elision behind dyn
:
-
Inside of function bodies: type inference.
-
&'r [mut] dyn Traits = &'r [mut] (dyn 'r + Traits)
- More generally, given
TyPath<dyn Traits>
whereTyPath<T : 'bound>
, we'll haveTyPath<dyn Traits> = TyPath<dyn 'bound + Traits>
.
- More generally, given
-
otherwise
dyn Traits = dyn 'static + Traits
Lifetime elision in impl Traits
Return-position -> impl Traits
(RPIT)
Similarly to dyn
, here, -> impl Traits ≠ -> impl '_ + Traits
.
But contrary to other properties/aspects of type erasure (where dyn
and -> impl
can be quite similar), when dealing with lifetimes / captured generics, impl Traits
happens to involve different semantics than those of dyn Traits
⚠️
See the dedicated section for more info.
Argument-position impl Traits
(APIT)
- (dubbed "universal" / caller-chosen)
Intuition: impl Traits
will "be the same as impl '_ + Traits
", that is, introducing a new named generic lifetime parameter <'usability>
, and then impl 'usability + Traits
. This aligns with the idea of it being a "universal" impl
type.
Rationale / actual semantics
These are kind of analogous to the following:
For each impl Traits
occurrence,
- introducing a new generic type parameter:
<T>
- bounded by the
Traits
:where T : Traits
- and replacing the
impl Traits
with that parameter:T
.
fn example(a: impl Send, b: impl Send)
// is the "same" as:
fn example<A, B>(a: A, b: B)
where
A : Send,
B : Send,
- the only difference will be that in the latter signature, callers will be able to use turbofish to specify the actual choices of
A
orB
, whereas in the former case this will be left for type inference.
Variance
Covariance: when "lifetimes" can shrink
So, in some of the previous chapters we've seen that sometimes, there is a seemingly silly operation which can be done: that of shrinking or shortening expiry-dates / regions / "lifetimes".
For instance, let's consider the simplest lifetime-infected type: a borrow (to, say, some primitive type):
&'borrow i32
// but also:
&'borrow mut i32
When you got your hands on such a thing with a 'long
borrow, if you have to feed it to some API that wants a 'short
borrow instead, you can!
use ::core::convert::identity;
fn demo<'short, 'long : 'short>(
r: &'long mut i32,
) -> &'short mut i32
{
identity::<&'short mut i32>(r) // ✅ OK!
}
This is called a subtyping relation, and you may observe that it is quite similar to an implicit coercion.
I'll personally be using the following syntax to express this subtyping property:
&'long mut i32 ➘ &'short mut i32
-
In the type-theory realm, they instead use
<:
for this, but I've never been very fond of this sigil, especially given how for lifetimes it's when we have'big ≥ 'short
that we have&'big () <: &'short ()
, which I find more confusing than saying&'big () ➘ &'short ()
.I like
➘
because:- it looks like a
->
without being exactly that one (so as to avoid ambiguities with function signatures); - we could almost interpret it as a "decay"ing operation, which I personally find to fit quite well the subtyping properties within actual code 🙂 (the amount of type information goes down as we fit into a supertype).
- it looks like a
Is shrinking lifetimes really "silly"?
Click to see this section
I guess the main example would be when trying to put together references with distinct lifetimes into a single collection such as a Vec
:
let local = String::from("…");
let names: Vec<&str> = vec![local.as_str(), "static str"];
Or, similarly, when type-unifying the output of two branches:
let storage: PathBuf;
let file_to_read: &Path =
match ::std::env::args_os().nth(1) {
| Some(s) => {
storage = s.into();
&storage
},
// no `.to_owned()` whatsoever needed in this branch 💪
| None => "default-file.toml".as_ref(),
}
;
Finally, remember the pick()
function section:
fn pick<'ret>(
left: &'ret str,
right: &'ret str,
) -> &'ret str
{
if … { left } else { right }
}
Imagine trying to call pick()
on a &String::from("…")
and a "static str"
: with this signature, the borrows are constrained to have equal lifetimes! Lifetime shrinkage to the rescue 🙂
Even in the more pedantic case of doing:
fn pick<'ret, 'left, 'right>(
left: &'left str,
right: &'right str,
) -> &'ret str
where
'left : 'ret,
'right : 'ret,
{
// Here, we have `'left` and `'right` lifetimes in branch;
// by covariance they get to shrink-unify down to some `'ret` intersection.
if … { left } else { right }
}
notice how the left
and right
expressions in each branch of the if
, respectively, by allowing the lifetime within each to shrink, managed to unify under a single shrunk lifetime, 'ret
(cough intersection cough).
Covariance, a definition
Covariant lifetime parameter
So, this property of certain generic lifetime parameters being allowed to shrink is called covariance (in that lifetime parameter). While most generic types will happen to be covariant, there is an important exception / counter-example which we need to be aware of:
- mutable…
- …borrows!
When both properties are "ticked"/met, then any lifetime in the borrowee is not (allowed) to be shrunk. We say of such a lifetime that it is non-covariant, or for short1, invariant.
- ⚠️ notice how we're talking of the lifetimes that may occur within the borrowee: the lifetime of the borrow itself is not part of the invariant stuff.
For instance, let's look at the type of that second argument of {Display,Debug}::fmt
function:
&'_ mut Formatter<'_>
Naming the lifetimes, we end up with:
// mutable borrow of
// vvvvvvvvvvvv
&'borrow mut Formatter<'fmt>
// ^^^^^^^^^^^^^^^
// invariant borrowee
// can shrink
// =
// covariant
// vvvvvvv
&'borrow mut Formatter<'fmt>
// ^^^^
// cannot shrink (nor grow)
// =
// invariant
A more typical albeit potentially visually confusing example is when the borrowee is itself a borrow:
&'borrow mut (&'fmt str)
In both cases, we have a &mut
-able 'borrow
, with thus a covariant/shrinkable lifetime, and behind such borrow, a thus invariant borrowee, resulting in 'fmt
not being allowed to shrink (nor grow).
-
Reminder that shared mutability exists
Note that the unique borrow,
&mut
, is not the only form of mutable borrow. Consider, for instance:// Covariant in `'borrow`, invariant in `'fmt`. &'borrow Cell<&'fmt str>
-
To gain a better intuition of this, I highly recommend that you open your mind to the following "statement":
use ::core::cell::Cell as Mut;
- (this is a legitimate thing to do since
Cell
does indeed let you mutate "its interior")
Thay way we end up with:
&'borrow Mut<&'fmt str>
- (this is a legitimate thing to do since
Finally, for technical reasons2, it turns out that the harmless owned shared mutability wrappers have to be declared invariant as well, despite the lack of borrows:
// Invariant too! Cell<&'_ str>
In practice, you're more likely to run into this when dealing with things like a
Mut ex
, aRwLock<_>
, or any form ofchannel
(at least theSender<_>
end)://! Invariant Mut ex<&'_ str> RwLock<&'_ str> RefCell<&'_ str> channel::Sender<&'_ str>
-
Covariant type parameter and composition rules
It turns out that saying:
- given
&'borrow /* Borrowee */
, - then, each lifetime occurring in
Borrowee
(if any), - is covariant;
is kind of a mouthful to say/write/express. We thus define this conventional term, when this occurs: we'll talk of &'borrow T
being covariant in the T
type parameter.
So a not-so-formal definition of type-parameter-covariance would be:
-
Determining whether a
<T>
-generic type is covariantA practical rule (of thumb) for determining whether a
<T>
-generic type is covariant (inT
) is to replaceT
by, say.&'s str
, and seeing the covariance (in's
) of the resulting type, since that will then be the covariance of the<T>
-generic type.Examples:
What is the covariance of
type Example<T> = (T, i32);
?-
We replace
T
with&'s str
: what is the covariance of(&'s str, i32)
? -
We somehow know that
type ExampleT<'s> = (&'s str, i32)
is covariant (in's
). -
"Thus",
Example<T>
is covariant (inT
).
-
What is the covariance of
type Example<T> = Arc<Cell<T>>;
?-
We replace
T
with&'s str
: what is the covariance ofArc<Cell<&'s str>>
? -
We know that "lifetimes behind a
Cell
are invariant", so we know that:type ExampleT<'s> = Arc<Cell<&'s str>>
is invariant (in's
). -
"Thus",
Example<T>
is invariant (inT
).
-
-
Composition rules: when you know that a
<T>
-generic type is covariant:For instance, consider
type Vec<T>
(or, more generally, anytype F<T>
).Now define some type depending on a lifetime parameter:
type T<'lt> = (i32, &'lt str, bool); // FYI, this is covariant.
Then the resulting type,
F<T<'lt>>
, i.e.,Vec<(i32, &'lt str, bool)>
, will have the same variance asT<'lt>
alone:<T>
-covariant types let the<'lt>
-variance insideT
pass through.
when also non-contravariant.
if Cell<T>
, owned, were covariant in T
, since &C
is covariant in C
, it would mean that &Cell<T>
. Which can't be. So Cell<T>
can't be covariant in T
.
Until now I've been talking of covariant / shrinkable lifetimes, or lack thereof (non-covariant / unshrinkable lifetimes).
But it turns out that, as surprising as it may initially seem, there are some specific types for which the lifetime parameter, rather than shrink, can actually grow.
Contravariance: when lifetimes can grow
The only time a lifetime is allowed to grow is when it occurs in function argument position:
-
type MyCb = fn(String);
which we have to make generic (reminder: variance is a property of generic types exclusively):
-
type MyGenericCb<Arg> = fn(Arg); // or: type MyLtGenericCb<'lt> = fn(&'lt str);
In this case, we'll say that MyGenericCb<Arg>
is contravariant in Arg
⇔ Lifetimes occurring in Arg
are allowed to grow ⇔ 'lt
is allowed to grow in MyGenericCb<&'lt str> = fn(&'lt str) = MyLtGenericCb<'lt>
⇔ MyLtGenericCb
is contravariant.
Growing lifetimes???
To get an intuition as to why/how can this case of growing lifetimes be fine, consider:
type Egg<'expiry> = &'expiry str; // or smth else covariant.
struct Basket<'expiry>(Vec<Egg<'expiry>>);
impl<'expiry> Basket<'expiry> {
fn stuff(
egg: Egg<'expiry>,
)
{
let mut basket: Self = Basket::<'expiry>(vec![egg]);
/* things with basket: Self */
drop(basket);
}
}
Now, imagine wanting to work with a Basket<'next_week>
, but only having an Egg<'next_month>
to construct it:
fn is_this_fine<'next_week, 'next_month : 'next_week>(
egg: Egg<'next_month>,
)
{
let stuff: fn(Egg<'next_week>) = <Basket<'next_week>>::stuff;
stuff(egg) // <- is this fine?
}
We have two dual but equivalent points of view that make this right:
-
Egg<'expiry>
is covariant in'expiry
, so we can shrink the lifetime insideegg
when feeding it: "we can shrink the lifetimes of covariant arguments right before they are fed to the function"; -
at that point "we can directly make the function itself take arguments with bigger lifetimes directly"
That is:
-
Given some
'expiry
lifetime in scope (e.g., at theimpl
level):fn stuff( egg: Egg<'expiry>, )
-
We could always shim-wrap it:
fn cooler_stuff<'actual_expiry>( egg: Egg<'actual_expiry>, ) where // ≥ 'actual_expiry : 'expiry, { Self::stuff( // since `Egg<'actual_expiry> ➘ Egg<'expiry>`. egg // : Egg<'expiry> ) }
-
So at that point we may as well let the language do that (let
stuff
subtypecooler_stuff
):
// until_next_month ⊇ until_next_week // 'next_month : 'next_week fn(Egg<'next_week>) ➘ fn(Egg<'next_month>)
-
That is:
fn(Egg<'short>) ➘ fn(Egg<'long>)
Composition rules for a contravariant type-generic type
The gist of it is that a type-generic contravariant type, such as:
type MyGenericCb<Arg> = fn(Arg);
will flip the variance of the stuff written as Arg
.
-
For instance, consider:
type Example<'lt> = fn(bool, fn(u8, &'lt str));
- Don't try to imagine this type used in legitimate Rust or you may sprain your brain 🤕
That is,
MyGenericCb<Arg<'lt>>
wheretype Arg<'lt> = fn(u8, &'lt str);
.type Arg<'lt> = fn(u8, &'lt str);
is contravariant;type MyGenericCb<Arg> = fn(bool, Arg)
is contravariant, so it flips the variance of the innerArg<'lt>
- That is,
MyGenericCb<Arg<'lt>>
is "contra-contravariant", i.e.,
is covariant.type Example<'lt> = fn(bool, fn(u8, &'lt str)) = MyGenericCb<Arg<'lt>>
Variance rules: a recap
Definition
Variance is a property of generic types:
-
Variance for a lifetime-generic type (e.g.,
Formatter<'_>
) will determine whether lifetimes can:- shrink (covariant),
- grow (contravariant),
- or neither (invariant).
-
Variance for a type-generic type (
Vec<T>
) will determine:-
intuitively, if that type parameter (
T
) were replaced with a lifetime-generic type itself (type T<'r> = &'r str
), how the variance of that innerT<'r>
type will propagate and affect the resulting variance of the composed type (Vec<T<'r>>
).× T<'lt>
Co-variantT<'lt>
Contra-variantT<'lt>
In-variantF<T>
covariant
=
"passthrough"Covariant F<T<'lt>>
Contravariant F<T<'lt>>
Invariant F<T<'lt>>
F<T>
contravariant
=
"flips it around"Contravariant F<T<'lt>>
Covariant F<T<'lt>>
Invariant F<T<'lt>>
F<T>
invariantInvariant F<T<'lt>>
Invariant F<T<'lt>>
Invariant F<T<'lt>>
Tip: if you see covariance as "being positive" (
+
), and contravariance as "being negative" (-
), and invariance as "being zero" (0
), these composition rules are the same as the sign multiplication rules!× +1 -1 0 +1 +1 -1 0 -1 -1 +1 0 0 0 0 0 -
formally, how the subtyping relation among two choices for the type parameter results in some subtyping relation for the generic type.
That is, if you have
T ➘ U
(T
a subtype ofU
), andtype F<X>;
- if
F
is covariant, thenF<T> ➘ F<U>
; - if
F
is contravariant, thenF<U> ➘ F<T>
(reversed!);
You can have
T ➘ U
by:-
common case: having a variant
type T<'lt>
, and picking the lifetimes accordingly (this is the intuitively section just above); niche case: having a higher-rank
fn
pointer typeand certain concrete choices of the inner generic lifetime:
//! Pseudo-code: //! using `fn<'any>(…)` instead of `for<'any> fn(…)` // From higher-order lifetime to a fixed choice: fn<'any>(&'any str) ➘ fn(&'fixed str) // From a covariant lifetime to a higher-order one: fn(fn(&'static str)) ➘ fn<'any>(fn(&'any str))
Interestingly enough, it does mean that if we pick:
-
type T = for<'any> fn(fn(&'any str)); type U = fn(fn(&'static str));
Then:
-
T ≠ U
; -
T : 'static
andU : 'static
; -
T ➘ U
by "fixing the higher-order lifetime"⚠️ A
: 'static
bound is not sufficient to prevent subtyping shenanigans form happening! ⚠️ -
U ➘ T
by "higher-ordering" a covariant lifetime by "induction from'static
".
-
- if
-
Main variance examples to keep in mind
- Mutable borrows and shared mutability wrappers are invariant.
Mutex<T>
,Cell<T>
,&'cov Cell<T>
,&'cov mut T
are invariant (inT
);
- otherwise, generally, owned stuff or immutably-borrowed stuff can be covariant.
T
,&'_ T
,Box<T>
,Arc<T>
, are covariant.
fn(CbArg)
is contravariant inCbArg
.- But
fn(…) -> Ret
is covariant inRet
!
- But
impl Traits<'lt>
anddyn Traits<'lt>
are invariant;- this includes the
Fn{,Mut,Once}
traits.
- this includes the
+ 'usability
is covariant:- Bonus: there is also a no-overhead-upcasting/reünsizing coercion possible from
&mut (dyn 'big + Traits)
to&mut (dyn 'short + Traits)
, which to the untrained eye could appear as if&mut (dyn 'lt + Traits)
were covariant in'lt
, which it is not.
- Bonus: there is also a no-overhead-upcasting/reünsizing coercion possible from
Variance of "product types" / struct
ural records / tuples
The rule of thumb is: combine them restrictively / non-covariance and non-contravariance are infectious.
By that I mean that you could think of variance as of marker traits:
trait Covariant {}
trait Contravariant {}
struct Example<'lt> {
x: &'lt str, // Covariant + !Contravariant
y: Mutex<&'lt bool>, // !Covariant + !Contravariant
} // !Covariant + !Contravariant = Invariant
For instance, in this Example
, we have a non-contravariant field alongside an invariant, i.e., neither-covariant-nor-contravariant field.
Thus, the resulting Example
can't be:
- contravariant, due to either field;
- covariant, due to the second field.
So we have a neither-covariant-nor-contravariant type, i.e., an invariant one.
Lifetime semantics of -> impl Trait
-> impl Trait
automagically captures generic type parameters in scope
fn foo<'lt, T>(lt: &'lt (), ty: T)
-> impl Sized
is sugar (and the only stable-Rust syntax at the moment) for:
#![feature(type_alias_impl_trait)]
fn foo<'lt, T>(lt: &'lt (), ty: T)
-> __ImplTrait<T>
// where the compiler has secretly generated:
pub type __ImplTrait<T> = impl Sized; // no `'lt` generic param!
- with
__ImplTrait<T>
being a compiler-generatedstruct
, of sorts, which is generic over<T>
and thus fully "infected byT
".
That is, it has the semantics of:
//! Pseudo-code
fn foo<'lt, T>(lt: &'lt (), ty: T)
-> impl Sized + InfectedBy<T> // no `+ InfectedBy<'lt>` !
What the helium is
InfectedBy<T>
?
It's pseudo-code of mine to express the property that this struct __ImplTrait<T>
would have: for any region 'r
, if the return type is usable within that region, then so must T
itself be:
for<'r> (
impl Sized + InfectedBy<T> : 'r
⇒
T : 'r
)
Although the practical/intuitive aspect of it is rather the contra-position:
for<'r> (
T : !'r // let's assume this is syntax to say that `T : 'r` does not hold.
⇒
impl Sized + InfectedBy<T> : !'r
)
To illustrate, let's pick T = &'t str
, for instance:
- beyond
't
, instances of typeT = &'t str
are not allowed to be used. - Then it means that beyond
't
, the returned-> impl Sized
won't be allowed to be used either (no matter the function body!).
These -> impl Sized + InfectedBy<T>
semantics are what allow the returned instance to contain ty: T
inside it.
But notice how, despite the 'lt
generic "lifetime" parameter in scope, there is no + InfectedBy<'lt>
in the unsugaring I've shown!
Example
The following works fine:
fn nothing_doër<'lt>(_: &'lt String) -> impl Fn() { || () } let f = { let local = String::from("…"); nothing_doër(&local) }; f() // ✅ OK
But the following does not:
fn nothing_doër<T>(_: T) -> impl Fn() { || () } let f = { let local = String::from("…"); nothing_doër(&local) }; f() // ❌ Error, `local` does not live long enough!
😬
-> impl Trait
does not implicitly capture generic "lifetime" parameters in scope
This is not an oversight of mine (nor of the language), but rather a deliberate choice of the design of -> impl Trait
. See the associated RFC for more info about it, and the rationale behind that choice.
The fact there is no + InfectedBy<'lt>
means the returned instance won't be allowed to contain lt: &'lt …
inside it.
fn foo<'lt, T>(lt: &'lt (), ty: T)
-> impl Sized
{
lt // ❌ Error, return type captures lifetime `'lt`
// which does not appear in bounds.
}
Error message
error[E0700]: hidden type for `impl Sized` captures lifetime that does not appear in bounds
--> src/lib.rs:11:5
|
8 | fn foo<'lt, T>(lt: &'lt (), ty: T)
| --- hidden type `&'lt ()` captures the lifetime `'lt` as defined here
...
11 | lt
| ^^
|
help: to declare that `impl Sized` captures `'lt`, you can add an explicit `'lt` lifetime bound
|
9 | -> impl Sized + 'lt
| +++++
- By the way that suggestion is not right, since it is not fully generalizable. See below for an example.
The workaround, currently, is to make the lifetime appear in bounds. If your trait in question does not give you room for that, then you can still achieve that by virtue of defining a helper trait for this, which happens to map quite nicely to my previous InfectedBy<…>
pseudo-code.
-
Define a helper generic-over-a-lifetime trait:
/// Dummy empty marker trait. pub trait InfectedBy<'__> {} impl<T : ?Sized> InfectedBy<'_> for T {}
-
Add it (
+ InfectedBy<…>
) to the bounds of the returned-> impl Trait
://! Real rust code fn foo<'lt, T>(lt: &'lt (), ty: T) -> impl Sized + InfectedBy<'lt> { lt // ✅ OK }
FWIW, you can imagine that this procedure is easily automatable through a macro:
About + 'lt
, a seemingly shorter, but not always applicable, workaround
In general, people will reach for a simple + 'lt
kind of bound (rather than the more cumbersome + InfectedBy<'lt>
).
But if you have understood the meaning of + 'usability
correctly, you should realize how this only works for simple cases:
/// ✅
fn simple_case<'lt>(lt: &'lt ())
-> impl 'lt + Sized
{
lt
}
/// ❌
fn does_not_work<'a, 'b>(a: &'a (), b: &'b ())
-> impl 'a + 'b + Sized
{
(a, b)
}
Error message
error: lifetime may not live long enough
--> src/lib.rs:21:5
|
18 | fn does_not_work<'a, 'b>(a: &'a (), b: &'b ())
| -- -- lifetime `'b` defined here
| |
| lifetime `'a` defined here
...
21 | (a, b)
| ^^^^^^ function was supposed to return data with lifetime `'b` but it is returning data with lifetime `'a`
|
= help: consider adding the following bound: `'a: 'b`
error: lifetime may not live long enough
--> src/lib.rs:21:5
|
18 | fn does_not_work<'a, 'b>(a: &'a (), b: &'b ())
| -- -- lifetime `'b` defined here
| |
| lifetime `'a` defined here
...
21 | (a, b)
| ^^^^^^ function was supposed to return data with lifetime `'a` but it is returning data with lifetime `'b`
|
= help: consider adding the following bound: `'b: 'a`
help: `'a` and `'b` must be the same: replace one with the other
Indeed, + 'usability
means that it has to be usable (at least) within the whole 'usability
region!
So if you write + 'a + 'b
it means that it will be usable within 'a
and1 that it will be usable within 'b
as well. That is, (an instance of (type)) impl 'a + 'b
has to be usable within the union of the regions 'a
and 'b
.
that's exactly what +
means in Rust: +
adds capabilities, rather than restraining them, so there is no way + 'a + 'b
yields, to the caller, something more restrictive than just + 'a
.
But the (a, b): (&'a (), &'b ())
value we are returning is meant to be unusable whenever:
- we leave the
'a
region (because of itsa
component), - we leave the
'b
region (because of itsb
component).
Which means (a, b)
is only usable within the intersection of the 'a
and 'b
regions.
-
In expiry dates parlance, imagine a small bottle of milk,
a
, with an expiry date of one month, and another such bottle,b
, but this time with an expiry date of one week. Say now that you mix the contents of both into a big bottle (this would be our(a, b)
Rust item). Then the resulting mix should be drunk or thrown away within the week, lest theb
part inside it go sour. That is, the resulting mix has an expiry date of'b = min('a, 'b) = intersection('a, 'b)
.An
impl 'a + 'b
bottle of milk would, on the other hand, have meant that it would have been drinkable both before next week, and before next month, so effectively before next month ('a = max('a, 'b) = union('a, 'b)
).
What we want to say is precisely that (a, b)
is infected by both 'a
and 'b
, i.e., that it is InfectedBy<'a> + InfectedBy<'b>
:
fn example<'a, 'b>(
a: &'a str,
b: &'b str,
) -> impl Fn(bool) + InfectedBy<'a> + InfectedBy<'b>
{
move /* (a, b) */ |choice| {
println!("{}", if choice { a } else { b })
}
}
Granted, the previous function can be simplified:
fn example<'intersection_of_a_and_b>(
a: &'intersection_of_a_and_b str,
b: &'intersection_of_a_and_b str,
) -> impl 'intersection_of_a_and_b + Fn(bool)
- since
'a
and'b
, from the call-sites, would be able to shrink down to some'intersection_of_a_and_b
region smaller than both, which would bring us back to a single-lifetime scenario, and thus to using the convenient+ 'usability
syntax to get the returned existential to be infected by'intersection_of_a_and_b
.
But in more complex APIs, lifetimes may not be able to shrink, and a proper solution needs to be used if you are to make it work.
For instance, no matter how hard you try, there is no simpler signature for the following function:
fn impl_<'a, 'b>(
a: Arc<Mutex<&'a str>>,
b: Arc<Mutex<&'b str>>,
) -> impl Fn(bool) + InfectedBy<'a> + InfectedBy<'b>
{
move /* (a, b) */ |choice| {
println!("{}", if choice {
*a.lock().unwrap()
} else {
*b.lock().unwrap()
});
}
}
Comparison to dyn Trait
Note that dyn Trait
is actual type erasure, so if you manage to get your hands on the 'intersection
lifetime of all the captured lifetime and type parameters, then you can simply use + 'intersection
and Rust won't complain even when bigger unshrinkable lifetimes are part of the before-erasure type (in other words, type erasure is kind of able to shrink even otherwise unshrinkable lifetimes):
fn dyn_<'i, 'a : 'i, 'b : 'i>( // intersection('a, 'b) ⊇ 'i
a: Arc<Mutex<&'a str>>,
b: Arc<Mutex<&'b str>>,
) -> Box<dyn 'i + Fn(bool)>
{
Box::new(move /* (a, b) */ |choice| {
println!("{}", if choice {
*a.lock().unwrap()
} else {
*b.lock().unwrap()
});
})
}
The keen eye may have spotted the "intersection lifetime" pattern. Otherwise, that newly introduced 'i
"lifetime" parameter will probably seem weird or confusing, in which case you may want to check out the dedicated section.
Intersection lifetime
Imagine wanting to write:
//! Pseudo-code
// this is what `#[async_trait]` wants to write, for instance.
fn async_fn<'a, 'b>(
a: Arc<Mutex<&'a str>>,
b: Arc<Mutex<&'b str>>,
) -> BoxFuture<'a ^ 'b, i32>
// 👆
// pseudo-code for `intersection('a, 'b)`
- See the
async fn
unsugaring section to know why.
Alas, we can't write that. Indeed, 'a ^ 'b
does not exist in current Rust (to keep things "simpler" I guess). So we have to resort to quantifications and set theory to express this property.
Behold, what peak "simpler" looks like:
Let A
and B
be two sets, and C
, their intersection: C = A ∩ B
Then, by definition, A ⊇ C
and B ⊇ C
(C
is a subset of A
and C
is a subset of B
).
In fact, one can define the intersection with that property:
-
Consider some set
I
which is a subset of bothA
andB
:I where A ⊇ I and B ⊇ I
-
visually, you can observe that
C ⊇ I
.
-
-
Now imagine the maximum of all possible
I
/ "try to makeI
bigger until you can't anymore": it'sC
!
A ∩ B = C = max { I where A ⊇ I and B ⊇ I }
Back to Rust, this becomes:
'a ^ 'b = max { 'i where 'a : 'i, 'b : 'i }
And two point of views are possible:
-
either we realize we don't necessarily need the max exactly, that something Big Enough™ would suffice. For instance, in our
dyn
example, we can realize that a subset of the actual intersection can suffice in practice (the subset where we actually use thedyn
).In this case, we can focus on this freely introduced
I
and all will be good. -
or we consider that a "free" extra generic parameter will be "min-maxed", as needed, by Rust's borrow checker, which is constantly trying to make our code pass/compile.
With this more magical but simpler point of view, we just introduce
I
and let Rust auto-maximize it as needed into the actual full intersection ofA
andB
:A ∩ B
.
'a ^ 'b ≈ 'i where 'a : 'i, 'b : 'i
In both cases we end up with the following recipe for something like an intersection "lifetime"/region of regions 'r0
, …, 'rN
:
Recipe for 'i = 'r0 ^ … ^ 'rN
-
<'i>
(let there beI
)That is, introduce
'i
as a new/added "free" generic region parameter. -
Add
where // ⊇ 'r0 : 'i, 'r1 : 'i, … 'rN : 'i,
as
where
clauses so as to guarantee thatI
is one of the{ I where R0 ⊇ I and … and RN ⊇ I }
. -
Use
'i
as if it were the desired intersection lifetime. -
Profit™
Example
We had:
fn async_fn<'a, 'b>(
a: Arc<Mutex<&'a str>>,
b: Arc<Mutex<&'b str>>,
) -> BoxFuture<'a ^ 'b, i32>
We apply the recipe:
-
// 👇 fn async_fn<'i, 'a, 'b>(
-
where 'a : 'i, 'b : 'i,
-
- ) -> BoxFuture<'a ^ 'b, i32> + ) -> BoxFuture<'i, i32>
That is:
// 👇 (1)
fn async_fn<'i, 'a, 'b>(
a: Arc<Mutex<&'a str>>,
b: Arc<Mutex<&'b str>>,
// 👇 (3)
) -> BoxFuture<'i, i32>
where
'a : 'i, // 👈 (2)
'b : 'i, // 👈
- (in the specific case of an
async fn
orFuture
, it may be more sensible to rename'i
as'fut
, or'async_fn
, or something along those lines)
async fn
unsugaring / lifetime
Consider, for instance:
trait Trait {
async fn async_fn<'a, 'b>(
a: Arc<Mutex<&'a str>>,
b: Arc<Mutex<&'b str>>,
) -> i32
{
dbg!(a.lock());
dbg!(b.lock());
42
}
}
As of 1.66.1
, in Rust, this cannot be written directly, since
async fn f(…) -> Ret
is sugar for
fn f(…) -> impl Future<Output = Ret>
and -> impl Trait
cannot be used in traits yet.
In the case of Future
, which is a dyn
-able trait, and which is Pin<Box>
-transitive (i.e., given T : ?Sized
, if T : Future
then Pin<Box<T>> : Future<Output = T::Output>
), a workaround for this limitation is thus to write the function as -> Pin<Box<dyn Future…>>
.
And since the process is:
- tedious,
- quite automatable,
- and yet a bit subtle at times,
we end up with —you guessed it— macros to do this for us, such as #[async_trait]
But how does #[async_trait]
do it?
-
Answering this question is more important than just intellectual curiosity. Indeed, what if:
- we didn't have access to it;
- we had to deal with a distinct trait or use case not covered by it;
- or ran into
#[async_trait]
bugs/limitations
To anticipate any of these cases, it's actually very sensible to learn to do what #[async_trait]
does.
-
So we start off
trait Trait { async fn async_fn<'a, 'b>( a: Arc<Mutex<&'a str>>, b: Arc<Mutex<&'b str>>, ) -> i32 { … } }
-
We replace
async fn … -> Ret
withfn … -> impl Future<Output = Ret>
:trait Trait { fn async_fn<'a, 'b>( a: Arc<Mutex<&'a str>>, b: Arc<Mutex<&'b str>>, ) -> impl Future<Output = i32> + InfectedBy<'a> + InfectedBy<'b> { async move { let _captures = (&a, &b); … } } }
- See
InfectedBy<'_>
- See
-
We
Pin<Box>
-wrap it:trait Trait { fn async_fn<'a, 'b>( a: Arc<Mutex<&'a str>>, b: Arc<Mutex<&'b str>>, ) -> Pin<Box< impl Future<Output = i32> + InfectedBy<'a> + InfectedBy<'b> >> { Box::pin(async move { let _captures = (&a, &b); … }) } }
-
We let it
dyn
:) -> Pin<Box< - impl Future<Output = i32> + dyn Future<Output = i32> + InfectedBy<'a> + InfectedBy<'b> >> { Box::pin(async move { let _captures = (&a, &b); … }) } }
- We won't bother with
Send
-ness, here.
- We won't bother with
-
We can't really use
+ InfectedBy<'_>
withdyn
(and don't really need to, as we'll see below), so we get rid of that too:dyn Future<Output = i32> - + InfectedBy<'a> + InfectedBy<'b> + + '???
At this point we've ended up with the following return type:
dyn '??? + Future<Output = i32>
And now the million dogecoin question is to know which lifetime we put here:
dyn 'a + Future…
?- May cover a region outside that of
'b
, so our future may dangle in there: ❌
- May cover a region outside that of
dyn 'b + Future…
?- symmetrical situation: ❌
dyn 'a + 'b + Future…
?- Papering over the fact Rust doesn't let us write
dyn 'a + 'b + …
for some reason, this cannot be right, since this expresses a'usability
that includes'a
and'b
, and we've seen that each of these is already problematic, so a bigger usability will be just as problematic, if not more!
- Papering over the fact Rust doesn't let us write
I've talked about all this in more detail over the section about -> impl Trait
, which also happens to mention the answer.
The gist of the answer is that:
An
impl InfectedBy<'a> + InfectedBy<'b>
is only usable within the intersection of'a
and'b
.InfectedBy<'a, 'b> : 'a ^ 'b
- Where
'a ^ 'b
is pseudo-code syntax to express the intersection of'a
and'b
.
In other words, our '???
/ the "'usability
" of such an entity is 'a ^ 'b
.
So we end up needing to write:
//! Pseudo-code
fn async_fn<'a, 'b>(
a: Arc<Mutex<&'a str>>,
b: Arc<Mutex<&'b str>>,
) -> BoxFuture<'a ^ 'b, i32>
This, as the non-existing-in-Rust 'a ^ 'b
syntax suggests, is not real Rust code. See the intersection lifetime section to know more about how the above ends up becoming:
//! Real-code!
fn async_fn<'fut, 'a, 'b>(
a: Arc<Mutex<&'a str>>,
b: Arc<Mutex<&'b str>>,
) -> BoxFuture<'fut, i32>
where
// ⊇
'a : 'fut, 'b : 'fut,
// 'a ^ 'b ⊇ 'fut
Appendix
(you're not really supposed to keep navigating linearly through the book at this point; what follows is just a series of standalone articles that the main parts of the book will refer to)
"Lifetime placeholders", or elided lifetimes
A lifetime can be elided:
- either implicitly, as is the case for
&str
or&mut i32
(behind a&
always lurks a lifetime parameter); - or explicitly, using the special
'_
syntax.
Across this book, I personally (overly) use the latter syntax, so as to show, clearly, all the elided lifetime parameters.
The result of an elided lifetime is thus a lifetime "hole", or lifetime placeholder, which will be the key thing to look at when thinking about lifetime elision rules. For instance, all these types involve lifetime holes:
&i32
,&'_ i32
,&mut i32
,&'_ mut i32
,GenericTy<'_>
1- e.g.,
Cow<'_, str>
,Formatter<'_>
,Context<'_>
,BoxFuture<'_, R>
,BorrowedFd<'_>
- e.g.,
dyn '_ + Trait
,dyn GenericTrait<'_>
, and ditto forimpl …
,- e.g.,
Pin<Box<dyn '_ + Send + Future<Output = R>>>
- e.g.,
Historically, it has been possible in Rust to use a lifetime-generic type without the <'_>
. But this can be very confusing, which is why since the edition 2018_idioms
, there is a elided_lifetimes_in_paths
lint which can be enabled (e.g., set to warn
), to catch these things. It is highly advisable to always set up this lint in any Rust project.
See the lifetime elision rules for more info.
The meaning of + 'lifetime
Subtyping vs. Coercions
-
Example of subtyping:
fn demo<'short>() { let s: &'static str = "…"; let s: &'short str = s; }
-
Examples of coercions:
fn implicit_coercions(a: &'_ mut Box<[u8; 4]>) { // `&mut` to `*mut` (& friends) let _: *mut _ = a /* as _ */; // `&mut` to `&` (this is not subtyping!) let b: &Box<[u8; 4]> = a /* as _ */; // `&impl Deref` to `&Deref::Output` let c: &[u8; 4] = b /* as _ */; // Unsized coercions: let _: &[u8] = c /* as _ */; let d: &dyn SubTrait = c /* as _ */; // `feature(trait_upcasting)` let _: &dyn SuperTrait = d /* as _ */; } // where: use ::core::any::Any as SuperTrait; trait SubTrait : SuperTrait {}
There are two differences between subtyping and implicit coercions:
-
Coercions are allowed to perform (basic) runtime operations to perform the conversion. For instance, the coercion from
&[u8; 4]
(thin data pointer to the beginning of 4 bytes) to&[u8]
(attaching a runtime4: usize
"field" next to that thin data pointer to end up with a wide pointer).Whereas subtypining relations cannot, so as to have the following property:
-
Subtyping relations can be nested/chained within arbitrarily deep complex types (provided that they be covariant), whereas coercions can't:
fn ok<'short, 'long : 'short>(v: Vec<&'long mut i32>) -> Vec<&'short mut i32> { v // ✅ OK } fn nope(v: Vec<&mut i32>) -> Vec<&i32> { v // ❌ Error }
The latter point, I'd say, is really the critical difference from a theoretical perspective, since, for instance, given some 'short
lifetime, consider the following "generic type" / type constructor:
<T> => &'short T
It is a rather simple type, which happens to be covariant in T
.
Covariance (using &'long X ➘ &'short X
)
Let's try with type T = &'long String;
, and trying to reach the type T = &'short String;
, since the former subtypes the latter:
&'short (&'long String) ➘ &'short (&'short String)
// by covariance (in T) of `&'short T`
// since &'long String ➘ &'short String
// since 'long ⊇ 'short
✅
Coercion? (using the &'long mut X ⇒ &'long X
coercion)
Now let's try with type T = &'long mut String
, and trying to go to type T = &'long String;
, since we know the former can be coerced to the latter:
fn test<'short, 'long : 'short>(
r: &'short (&'long mut String),
) -> &'short (&'long String)
{
r /* as _ */
}
This errors! ❌
Error message
error[E0308]: mismatched types
--> src/lib.rs:5:5
|
3 | ) -> &'short (&'long String)
| ----------------------- expected `&'short &'long String` because of return type
4 | {
5 | r /* as _ */
| ^ types differ in mutability
|
= note: expected reference `&'short &'long String`
found reference `&'short &'long mut String`
Not only does it error, but there is actually no way to write this soundly.
Indeed, such operation is unsound, i.e., if it existed, it would make it possible, using it and non-unsafe
Rust, to trigger UB/memory-corruption:
-
Given:
fn unsound<'short, 'long : 'short>( r: &'short (&'long mut String), ) -> &'short (&'long String) { unsafe { ::core::mem::transmute(r) } }
-
One can write:
/// Here is an example of what `unsound` lets us do: fn exploit_helper<'long>( s: &'long mut String, ) -> ( &'long String, &'long mut String, ) { // `s` cannot be used while `short` (or data // derived from it) is being used. let short: &'_ &'long mut String = &s; // the `'short` lifetime in `changed` is still // derived from `short`, so `s` still can't be used let changed: &'_ &'long String = unsound(short); // But we can now dereference `changed`, // since `&'long String : Copy`, // and the resulting `&'long String` is no longer // tied to `short`. let r: &'long String = *changed; // That is, we can freely use `s` even if `r` is used too! (r, s) }
Which means we'd have a (shared) reference aliasing with an exclusive one! UB
-
For instance:
let s = &mut String::from("Hey!"); let (r, s) = exploit_helper(s); // Have `r` point to the heap-allocated utf-8 bytes the `String` owns. let r: &str = &*r; // And now let's deallocate those bytes: *s = String::new(); // read deallocated memory: dbg!(r);
error: Undefined Behavior: trying to retag from <3360> for SharedReadOnly permission at alloc1685[0x0], but that tag does not exist in the borrow stack for this location --> src/main.rs:19:5 | 19 | (r2, r) | ^^^^^^^ | | | trying to retag from <3360> for SharedReadOnly permission at alloc1685[0x0], but that tag does not exist in the borrow stack for this location | this error occurs as part of retag at alloc1685[0x0..0x18] | = help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental = help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information help: <3360> was created by a SharedReadOnly retag at offsets [0x0..0x18] --> src/main.rs:19:6 | 19 | (r2, r) | ^^ help: <3360> was later invalidated at offsets [0x0..0x18] by a Unique retag --> src/main.rs:19:10 | 19 | (r2, r) | ^ = note: BACKTRACE (of the first span): = note: inside `exploit_helper` at src/main.rs:19:5: 19:12 note: inside `main` --> src/main.rs:25:18 | 25 | let (r, s) = exploit_helper(s); | ^^^^^^^^^^^^^^^^^