What does the trait bound 'static
mean in Rust?
After a few weeks of programming in Rust we should have a pretty good idea of what a 'static
reference is, but a 'static
trait bound can seem a bit more mysterious.
The first time I had to add a 'static
trait bound to my code. I didn't feel like I understood what was happening, because there were no references involved. In fact, the only data involved were function arguments being passed by ownership, which is usually a pretty reliable way of avoiding borrow checker problems.
In many cases, the rust compiler will suggest a 'static
trait bound to address a problem. The compiler is probably right, but let's explore what it really means so we can be confident that we understand the solution.
Part 1: We introduce a bug and the compiler helps us fix it. §
Let's use this code as our example:
pub fn read_in_background(mut f: File) {
thread::spawn(move || {
let mut buf = Vec::<u8>::new();
if let Ok(count) = f.read_to_end(&mut buf) {
println!("read {} bytes from file.", count);
}
});
}
That function works fine. However, I would like to be able to test the code but don't want my tests to actually create real files. Tests sometimes crash, after all, and it's a pain to ensure that tests always clean up after themselves. It's also no fun to debug unreliable test automation because the box ran out of scratch space due to tests leaking temporary files.
Instead, let's make read_in_background
generic across a broader set of types:
pub fn read_in_background<T: Read>(mut f: T) {
thread::spawn(move || {
let mut buf = Vec::<u8>::new();
if let Ok(count) = f.read_to_end(&mut buf) {
println!("read {} bytes from file.", count);
}
});
}
This change makes the compiler sad, though:
error[E0277]: `T` cannot be sent between threads safely
--> src/lib.rs:6:5
|
6 | thread::spawn(move || {
| _____^^^^^^^^^^^^^_-
| | |
| | `T` cannot be sent between threads safely
7 | | let mut buf = Vec::<u8>::new();
8 | | if let Ok(count) = f.read_to_end(&mut buf) {
9 | | println!("read {} bytes from file.", count);
10 | | }
11 | | });
| |_____- within this `[closure@src/lib.rs:6:19: 11:6]`
The Rust compiler has a suggestion to improve our function: because we're calling thread::spawn
, it would like our data to be safe to send between threads.
help: consider further restricting this bound
|
5 | pub fn read_in_background<T: Read + Send>(mut f: T) {
That suggestion seems sensible. Let's do what it says; we'll add a Send
bound to our generic type.
pub fn read_in_background<T: Read + Send>(mut f: T) {
thread::spawn(move || {
let mut buf = Vec::<u8>::new();
if let Ok(count) = f.read_to_end(&mut buf) {
println!("read {} bytes from file.", count);
}
});
}
The compiler is still unsatisfied, though:
error[E0310]: the parameter type `T` may not live long enough
--> src/lib.rs:6:5
|
5 | pub fn read_in_background<T: Read + Send>(mut f: T) {
| -- help: consider adding an explicit lifetime bound...: `T: 'static +`
6 | thread::spawn(move || {
| ^^^^^^^^^^^^^ ...so that the type `[closure@src/lib.rs:6:19: 11:6]` will meet its required lifetime bounds
The compiler is unhappy about lifetimes, and suggests we add a trait bound 'static
.
The compiler's help
message has suggested the right thing (though it's not yet clear how or why). First, let's do what it says, just to see that it does work:
pub fn read_in_background<T: Read + Send + 'static>(mut f: T) {
thread::spawn(move || {
let mut buf = Vec::<u8>::new();
if let Ok(count) = f.read_to_end(&mut buf) {
println!("read {} bytes from file.", count);
}
});
}
And the compiler is happy! I can write tests that use std::io::Cursor
instead of File
, and now my tests don't need to write things to the filesystem.
Part 2: What just happened, anyway? §
That 'static
nags at me. I passed the T
(File
or Cursor
or whatever) by transferring ownership. There were no references in use, so why is a lifetime bound needed?
The answer is that when we create a generic type T
, we need to convince the compiler that our function must work for all possible types (that satisfy the bounds Read + Send
). Some of those types might be references! Or might include references internally.
Let's build an alternate universe version of our function, before we made it generic:
pub fn read_in_background(f: &mut File) {
thread::spawn(move || {
let mut buf = Vec::<u8>::new();
if let Ok(count) = f.read_to_end(&mut buf) {
println!("read {} bytes from file.", count);
}
});
}
The only thing I changed is that the argument is now type &mut File
instead of File
, so we're passing in a reference with an un-specified lifetime.
The compiler dislikes this, and expresses itself with a familiar message:
error[E0621]: explicit lifetime required in the type of `f`
--> src/lib.rs:6:5
|
5 | pub fn read_in_background(f: &mut File) {
| --------- help: add explicit lifetime `'static` to the type of `f`: `&'static mut std::fs::File`
6 | thread::spawn(move || {
| ^^^^^^^^^^^^^ lifetime `'static` required
This code could have worked, except that anything sent through the wormhole into thread::spawn
may need to live for an arbitrary amount of time. The only solution available to us is to tag that reference with 'static
to ensure we don't violate memory safety rules. (Alternatively, we could go looking for a scoped thread implementation.)
Here's an interesting fact: the
Read
trait is implemented for&File
! Had the compiler not forced us to add a'static
bound, our generic implementation withT: Read + Send
could accept&File
without being able to prove a correct lifetime.
The way that I often think about the 'static
trait bound is:
"I don't want my generic type T
to permit reference types."
That's not strictly true, because we know it will allow references that can live forever. But for the sake of simplicity, when I want to pass ownership of a real value, I don't want to think about the compiler allowing references under certain peculiar conditions. Instead I am going to use a lazy mental shortcut, and allow myself to believe that 'static
means "don't allow references".
There's another possible case, which is a type that has internal references. For example:
struct MyCursor<'a> {
data: &'a [u8],
}
impl<'a> Read for MyCursor<'a> {
fn read(&mut self, buf: &mut [u8]) -> std::result::Result<usize, std::io::Error> {
let size = min(buf.len(), self.data.len());
buf[..size].copy_from_slice(&self.data[..size]);
Ok(size)
}
}
This is perhaps a bit silly, because std::io::Cursor<&[u8]>
already exists. But it illustrates the point: even if we try to pass ownership of MyCursor
to our generic read_in_background
, the compiler should prevent us.
This provides a slightly less lazy way of thinking about the T: 'static
trait bound: imagine that any potential T
looks like this:
struct Thing<'a> {
...
}
We are using the bound T: 'static
to restrict Thing<'a>
to Thing<'static>
. If the type doesn't have any generic lifetime parameters, then that bound is automatically satisfied. It's as though every data structure without a generic lifetime parameter has an implicit <'static>
attached.
Part 3: Doing the same thing, with a custom trait. §
The same basic principle applies if I want read_in_background
to be part of a trait.
pub trait BgRead {
fn read_in_background(self);
}
impl<T> BgRead for T
where
T: Read + Send + 'static,
{
fn read_in_background(mut self) {
// ...
}
}
This works fine. It's also possible to attach some or all of the trait bounds to the trait definition itself:
pub trait BgRead: Send + 'static {
fn read_in_background(self);
}
Why attach the trait bounds to the trait definition, if it's not necessary to make the code work? I can think of a few reasons:
- Maybe BgRead would never make sense for reference types, and we want to convey that expectation to humans reading the code.
- The compiler will complain right away if we accidentally allow a reference type. This might save us a little time when adding new
impl
s, by making the mistake obvious.
To illustrate this, imagine we decide to implement the BgRead
trait for any type Into<String>
:
impl<T> BgRead for T
where
T: Into<String>,
{
fn read_in_background(mut self) {
thread::spawn(move || {
let s: String = self.into();
println!("read {} bytes from string.", s.len());
});
}
}
We won't get very far with this code, because we try to send self
to another thread (which will require Send + 'static
), and the compiler will say so clearly. If our code were more complex, it might not be as obvious what's wrong— perhaps the value gets passed down through layers of generic functions before the error appears.
It might save some time if we communicate with future implementers of BgRead
by putting those trait bounds on the trait itself. If they turn out to be wrong, it will probably be easy to loosen the bounds on the trait later.
See also Rust RFC 2089, implied_bounds, which would allow us to omit the trait bounds on an
impl
if they were already expressed in the trait itself.
I hope this was helpful! Good luck with your Rust projects.