What does it mean to return `dyn Error + 'static`?

error::source is defined as

fn source(&self) -> Option<&(dyn Error + 'static)>

From the book I thought that 'static meant

One special lifetime we need to discuss is 'static, which denotes the entire duration of the program.

The most understandable example of this, for me, is static strings or other constants, which obviously live the entire life of the program.

But in the library's example of Error::source, it returns a reference to a member of self:

    fn source(&self) -> Option<&(dyn Error + 'static)> {
        Some(&self.side)
    }

How is this 'static?

'static can mean either what is discussed in the book as you mentioned, or, it may sometimes mean that a type or trait object satisfies that lifetime.

That is to say, that if I (could; since you can't own trait objects directly) have x: dyn Trait + 'static, then my x could theoretically last for the entirety of the program's lifetime. This is usually explained as "x contains no references" but that's not true; since this type is 'static:

struct Foo {
    bar: &'static u8
}

More reading

You don't need to only use 'static for this though; you can actually see this used sometimes for other types:

struct Bar<'x> {
    x: Option<&'x usize>
}

fn foo<'a: 'a, 'b, T: Iterator>(x: T, u: &Bar<'_>) where T::Item: 'a {
    u.x = x.next();
}

Or, in associated types:

trait VertexProcessor<'a> {
    type VertexArray: 'a;
}

impl<'a> VertexProcessor<'a> for MyStruct {
    type VertexArray = &'a [u8];
}

This is a workaround for missing generic associated types (GAT).

1 Like

I think I half understand.

I suppose the intention of this is to say that the error returned from source can last longer than the error of which it is the source? That would make sense. Concretely, it cannot hold references into the downstream error. So if this wasn't there, the implied lifetime would be something like this, which is too constraining:

fn<'a> not_source(&'a self) -> &'a dyn Error;

The returned error need not, itself, live forever (in the way a static string does) but it must be capable of living arbitrarily long, because it does not hold references to anything that dies sooner than the object itself?

Is this talking about the difference between

&'static dyn Error

versus

&(dyn Error + 'static)

I feel like this could be clearer in the book...

1 Like

... further, the section you link says

  • The default lifetime of a trait object is 'static .

In this definition, dyn Error is a trait object, and no lifetime is specified, so why doesn't it have 'static lifetime by default?

1 Like

If you're talking about

x: &'a dyn Error

Then it's implicit that it must live for at least as long as 'a. But actually this is still dyn Error + 'static; since for any 'a, 'static: 'a.

In other words; the following pseudocode is okay:

let x: &'static T = /**/;
let y: &'x &'static T = &x;

It's implicit that 'static: 'x, or in other words 'static > 'x.


I misinterpreted the question; please read @mbrubeck's answer below.

My question is, why is it

fn source(&self) -> Option<&(dyn Error + 'static)>

not

fn source(&self) -> Option<&dyn Error>

'static is the default lifetime bound for Box<dyn Trait> but not for &dyn Trait.

fn source(&self) -> Option<&dyn Error>

is equivalent to

fn source<'a>(&'a self) -> Option<&'a dyn Error + 'a>

For details, see Default Trait Object Lifetimes in the Rust Reference.

3 Likes

Right, thanks. That's what I meant with the not_source above.

Perhaps it's just me, but I find it hard to understand from the Default Trait Object Lifetimes section why a default of 'a is implied here.

If I try to read fn source(&self) -> Option<&dyn Error> against that section:

These default object lifetime bounds are used instead of the lifetime parameter elision rules defined above when the lifetime bound is omitted entirely. If '_ is used as the lifetime bound then the bound follows the usual elision rules.

It's a trait object, and the lifetime bound is omitted. So, the usual elision rules do not apply.

If the trait object is used as a type argument of a generic type then the containing type is first used to try to infer a bound.

  • If there is a unique bound from the containing type then that is the default
  • If there is more than one bound from the containing type then an explicit bound must be specified

Ah, ok, perhaps this is it? dyn Error is a type argument of generic Option<T>. And, by the usual elision rules, 'self is inferred for Option<T>. Then the bound from the Option' propagates down to the dyn Error, and to avoid that it has to have an explicit 'static`.

So a hypothetical alternative function without the Option would be static if nothing was specified:

fn mandatory_source(&self) -> &dyn Error // implicitly static

fn boxed_source(&self) -> Box<dyn Error> // Box puts no lifetime bound on contents
fn mandatory_source(&self) -> &dyn Error

This is shorthand for:

fn mandatory_source<'a>(&'a self) -> &'a dyn Error

In this case the containing type is &'a T which has a single lifetime parameter 'a, so the default lifetime bound on the trait object is 'a. That is, this is equivalent to &'a dyn Error + 'a.

fn boxed_source(&self) -> Box<dyn Error>

In this case the containing type is Box<T> which has no lifetime parameter, so the default trait object bound is 'static. That is, this is equivalent to Box<dyn Error + 'static>.

2 Likes

The short version is:

  • Box<dyn Trait> has an implicit 'static bound.
  • &dyn Trait does not.

The Option does not factor into this, since it doesn't directly contain the trait object.

  • Option<Box<dyn Trait>> has an implicit 'static bound.
  • Option<&dyn Trait> does not.

As for why the Error::source method imposes this 'static bound. the reason as explained in RFC 2504 is to support downcasting the trait object back to a concrete type.

Downcasting only works for 'static types, basically because of implementation details. The compiler needs to generate a unique ID at compile time for each type that can be downcast. It doesn't have a way to do this for types with lifetime parameters because the "concrete" lifetimes can't be enumerated at compile time.

4 Likes

Given some generic lifetime parameter 'borrow,

  • the type &'borrow dyn Error is actually sugar for &'borrow (dyn Error + 'borrow).

So, you question becomes:

&'static (dyn Error + 'static)

vs.

&'borrow (dyn Error + 'static)

For which the answer should now be quite clear: the former is not tied to 'borrow and can thus be held arbitrarily long (e.g., by another thread). This is called "being 'static" and is written as follows:

&'static (dyn Error + 'static) : 'static
  • read as: a 'static borrow to an object that itself is 'static (i.e., that can be held arbitrarily long), can itself (the borrow!) be held arbitrarily long.

whereas the latter is tied to the 'borrow lifetime, so
&'borrow (dyn Error + 'static) : 'borrow

  • read as: as reference with lifetime 'borrow to something that can be held (owned) arbitrarily long can itself (the borrow!) only be held for the lifetime borrow. That's because the borrow is the most restrictive here thing, and this property is paramount for the soundness. Even if the pointee could be owned arbitrarily long, we do not own it, so it may not live that long (imagine let x = 42; &x: integers can live arbitrarily long when owned but the &x reference itself cannot, it won't be valid after x is dropped).

I think your real question / where the confusion comes from is instead regarding the two following types:

&'borrow (dyn Error + 'static)
  • the pointee, when owned, can be held arbitrarily long,

vs.

&'borrow (dyn Error + 'borrow)
  • the pointee, when owned, can be held only for the lifetime 'borrow,

Since both types are : 'borrow only (they can only be held for as long as the borrow is active / the lifetime 'borrow) it can be hard to tell the difference. In the case of the Error trait, there is "actually no practical difference" (even if these two types are distinct!), except for a current technical limitation having to do with downcasting (a very important feature with type erased errors!), as @mbrubeck very aptly pointed out.


Now, let's try to better grasp the difference between &'short (dyn Trait + 'short) and &'short (dyn Trait + 'static).

FIrst of all, by variance (an advanced topic), the latter subtypes the former, meaning that whatever the former can do, the latter can also do. So if there is a difference, it is in something that can be done with the latter but that the former cannot do.

Since, as I mentioned, the difference lies in owning the pointee, let's use an operation that goes fromnm &'_borrow (dyn Trait + '_self) to dyn Trait + '_self (a clone for dyn Trait + '_self). Here is an example:

// i32 : Trait

fn former<'borrow> (it: &'borrow i32) -> &'borrow (dyn Trait + 'borrow)
{
    it
}

fn latter<'borrow> (it: &'borrow i32) -> &'borrow (dyn Trait + 'static)
{
    it
}

fn clone<'borrow, '_self> (it: &'borrow (dyn Trait + '_self))
  -> Box<dyn Trait + '_self>
{
    it.clone_box()
}

fn main ()
{
    let (_cloned_x_former, _cloned_x_latter) = {
        let x = 42;
        let former = former(&x); // &'borrow (dyn Trait + 'borrow)
        let latter = latter(&x); // &'borrow (dyn Trait + 'static)
        (
            clone(former), // Box<dyn Trait + 'borrow>
            clone(latter), // Box<dyn Trait + 'static>
        )
        // <-- 'borrow cannot go beyond this scope
    }; // error _cloned_x_former: Box<dyn Trait + 'borrow> cannot outlive `'borrow`
}

I think it is best to study that code in detail, and I expect an "aha!" moment to spontaneously happen afterwards: you ought to grasp the difference between the two types and the two lifetime spots :slightly_smiling_face:


And here is a Playground where "_self" is not 'static, to see these lifetimes annotations on dyn itself are paramount for soundness.

5 Likes

Thanks for the generously detailed explanations.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.