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.

Rust Logo

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 with T: 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 impls, 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.

Comments? Get in touch on twitter: @codeandbitters