Consider the following code:
trait Trait<T> {}
fn foo<'a>(_b: Box<Trait<&'a usize>>) {}
fn bar(_b: Box<for<'a> Trait<&'a usize>>) {}
Both functions foo
and bar
seem to accept a Box<Trait<&'a usize>>
, although foo
does it more concisely than bar
. What is the difference between them?
Additionally, in what situations would I need for<>
syntax like that above? I know the Rust standard library uses it internally (often related to closures), but why might my code need it?
for<>
syntax is called higher-ranked trait bound (HRTB), and it was indeed introduced mostly because of closures.In short, the difference between
foo
andbar
is that infoo()
the lifetime for the internalusize
reference is provided by the caller of the function, while inbar()
the same lifetime is provided by the function itself. And this distinction is very important for the implementation offoo
/bar
.However, in this particular case, when
Trait
has no methods which use the type parameter, this distinction is pointless, so let's imagine thatTrait
looks like this:Remember, lifetime parameters are very similar to generic type parameters. When you use a generic function, you always specify all of its type parameters, providing concrete types, and the compiler monomorphizes the function. Same thing goes with lifetime parameters: when you call a function which have a lifetime parameter, you specify the lifetime, albeit implicitly:
And now there is a restriction on what
foo()
can do with this value, that is, with which arguments it may calldo_something()
. For example, this won't compile:This won't compile because local variables have lifetimes which are strictly smaller than lifetimes specified by the lifetime parameters (I think it is clear why it is so), therefore you can't call
b.do_something(&x)
because it requires its argument to have lifetime'a
, which is strictly greater than that ofx
.However, you can do this with
bar
:This works because now
bar
can select the needed lifetime instead of the caller ofbar
.This does matter when you use closures which accept references. For example, suppose you want to write a
filter()
method onOption<T>
:The closure here must accept a reference to
T
because otherwise it would be impossible to return the value contained in the option (this is the same reasoning as withfilter()
on iterators).But what lifetime should
&T
inFnOnce(&T) -> bool
have? Remember, we don't specify lifetimes in function signatures only because there is lifetime elision in place; actually the compiler inserts a lifetime parameter for each reference inside a function signature. There should be some lifetime associated with&T
inFnOnce(&T) -> bool
. So, the most "obvious" way to expand the signature above would be this:However, this is not going to work. As in the example with
Trait
above, lifetime'a
is strictly longer than the lifetime of any local variable in this function, includingvalue
inside the match statement. Therefore, it is not possible to applyf
to&value
because of lifetime mismatch. The above function written with such signature won't compile.On the other hand, if we expand the signature of
filter()
like this (and this is actually how lifetime elision for closures works in Rust now):then calling
f
with&value
as an argument is perfectly valid: we can choose the lifetime now, so using the lifetime of a local variable is absolutely fine. And that's why HRTBs are important: you won't be able to express a lot of useful patterns without them.You can also read another explanation of HRTBs in Nomicon.