Idea: aliasing the `'static` lifetime for lifetime parameters / trait bounds, e.g., `'auto`

From chapter 15.4.8. Static in Rust By Example, we can learn that:

You might encounter 'static in two situations:

  • A reference with 'static lifetime
  • 'static as part of a trait bound

Both are related but subtly different and this is a common source for confusion when learning Rust.

and:

As a reference lifetime 'static indicates that the data pointed to by the reference lives for the entire lifetime of the running program.

while:

As a trait bound, it means the type does not contain any non-static references. Eg. the receiver can hold on to the type for as long as they want and it will never become invalid until they drop it.


I mean, why should we have both lifetimes that are conceptually different share the same name 'static?

And personally I think the name 'static is counter-intuitive for the latter case. A lifetime that ends when the value is dropped is the "normal" lifetime for a reference, but it gets called 'static in trait bounds?

It took me a great while to figure out why String does implement Into<Cow<'static, str>> although most of the time a String value does not have a 'static lifetime (as in, they live for the entire lifetime of the running program).

This isn't two different meanings of 'static; it is two different meanings of mentioning any lifetime.

  • T: 'static means that T contains no references that aren't valid for 'static.
  • T: 'a means that T contains no references that aren't valid for 'a.

Certainly this is a frequent point of confusion, and there could probably be better documentation, but 'static, specifically, is not special here — so renaming 'static won't help.

1 Like

Yeah, I think the thing to improve here is how Rust By Example explains it. The page strongly implies that Rust has used the same name, 'static, for 2 different meanings, but they are actually 2 different usages of the same meaning, as kpreid explained.

It's understandable why someone might have written "Both are related but subtly different and this is a common source for confusion when learning Rust," but I don't think documentation benefits from spending time on saying "this is hard" rather than explaining. Instead, it should define it and explain, perhaps saying something like "the 'static lifetime is the lifetime that lasts the whole duration of the program. Here are the common situations where it's used:"

Ah! That's where the confusion begins.

When we use &'static T, it means the lifetime of the reference is 'static (always valid), and the value must outlive its reference.

But since we're all taught that there's a lifetime for every value in Rust, reference/borrowed or owned, an intuitive (but inaccurate) comprehension of the lifetime annotation 'a without & would be:

  • for structs, say struct Combined<'a, 'b>, the lifetime of a Combined<'a, 'b> value cannot exceed that of either 'a or 'b, so if the value contains a field with lifetime 'a and a field with lifetime 'b, you ought to write struct Combined<'a, 'b>, while struct Combined<'static, 'static> is a mistake and would not work.
  • for trait impls, like impl<'a> From<String> for Cow<'a, str>, the lifetime of a From<String> value whose underlying type is Cow<'a, str> cannot exceed that of 'a. So if a Cow<'a, str> does not have a 'static lifetime, the From<String> trait to get the value cannot have a 'static lifetime, so the 'a written here is not 'static.

In real life however, Into<Cow<'static, str>> is implemented for String, meaning From<String> is implemented for Cow<'static, str>, even though the Cow value is just an Owned variant with the owned String value itself, and is not guaranteed to have a 'static (always valid) lifetime.


Back to your explanation (sorry, I have to repeat it):

What you said is certainly true: the Cow<'static, str> value converted From<String> contains no references that aren't valid for 'static.

Since the only value inside the Cow<'static, str> we got (apart from the enum discriminator) is owned, it contains no references at all!


And that's exactly why I think it's counter-intuitive. Just think of it:

  1. When we annotate reference lifetimes, we use &'a, and it corresponds to the lifetime of the value (in this case, the value means the reference, not the underlying owned value).
  2. When we add a lifetime parameter to a struct / enum, we use 'a, and it corresponds to the lifetime of the struct / enum value.
  3. When we specify trait bounds, we use 'a, and it corresponds to the lifetime of the value implementing the trait.

While theoretically for case 2 and 3, 'a can strictly outlive any reference inside the value, we normally can't achieve that because we can't create a named lifetime out of thin air.

With 'static, things are totally different. 'static strictly outlives anything non 'static, and it's practically the only case most Rustaceans would ever see a lifetime parameter / trait bound behaves this way.


I got your point by saying "it's not different behaviors of 'static being counter-intuitive, it's the difference between reference lifetime annotation and lifetime parameters / trait bounds being counter-intuitive".

(When I say counter-intuitive, I mean a longer lifetime in lifetime parameters / trait bounds is a stronger requirement for a reference and you can't meet that requirement implicitly. But a longer lifetime in the same place, say 'static, is automatically valid for an owned value which can be non-'static, and this difference is not intuitive)

Although what you stated is true, since normal lifetimes don't have a chance to show the "counter-intuitive" part, they effectively leave 'static out as the special one.


Actually this reminds me of the interface {} type in Go. There's nothing wrong with writing interface {} since you can also write struct {int; byte} as a type name, and interface {} is something just like it.

But IRL non-empty anonymous interfaces are rarely seen, so interface {} becomes special. Without peers, the syntax fails to look intuitive and the Go team figured they'd replace the anonymous interface {} with the alias any to make it more like an ordinary type name instead of looking like a type holding interface definitions.


The problem with 'static is, IMO, similar. While it follows the same rules as any other lifetime, its usage is different from them, so the weirdness of the rules itself didn't get exposed by other lifetimes.

Instead of teaching everybody about all the details and expect people to understand (which I seriously doubt), I think an alias is worth it.

What about 'auto as an alias for 'static only when used in lifetime parameters / trait bounds?

To people who're not that familiar with the rules, 'auto means it is the auto-inferred explicit lifetime parameter ('static / always valid for references, and live until getting dropped for owned values).

To people who know what's under the hood, 'auto is just an alias for 'static, like any for interface {} in Go.

Actually I'm ok with this kind of "inaccurate" explanations, even though I now understand what's explained by kpreid.

It's similar to "although the theory of relativity is more accurate, it just fine for most people to just learn classical mechanics to meet the needs in daily life".

And that's exactly why I suggest an alias to be added - to make "classical mechanics" even more easy to learn.

I'm not sure what you're trying to get at here. Trait implementations don't have lifetimes, lifetimes are merely involved in order to be generic over the lifetimes of the type/trait being implemented.

I think you're creating the special case for references here. When you have a &'a:

  • the reference itself can't live more than 'a, so you could say it's its lifetime, but is also not exactly its lifetime because the reference could live less
  • the referenced value (which is what the refernece borrows) must live at least 'a, so again these is a relation with 'a but it's not perfect because the value could live more

Likewise for struct/enums parameterized over a lifetime 'a:

  • a value of the struct/enum itself can't live more than 'a, but could also live less
  • whatever borrow the struct/enum contains must live at least 'a, but could live more

So there's a perfect simmetry between the two.

I don't think this ever corresponds to the lifetime of a specific value, because there's no value when the lifetime bound is used. The way I see it is that it requires that the type T to be valid in the lifetime 'a, in other words that T has no requirement for its values to live for less than 'a (which can only happen due to lifetime parameters of T, but that would be a more ad-hoc explanation). The intended meaning is that given T: 'a and an instance of T, you can hold onto that instance of T for the whole 'a without worrying about that instance becoming invalid. Thus instances of T are valid for at least 'a, which sounds very similar to the previous points (whatever borrow a reference/struct/enum contains, it lives for at least 'a)

The way I see 'static being different than other lifetime annotations is that normal lifetime annotations are like unbound variables and you always talk generically about them, while 'static is a concrete lifetime that exist by itself. This holds in every context though, be it a reference's lifetime, a struct/enum lifetime or a lifetime bound.

Actually, there is a lifetime that's similar in this regard, and that's '_, the elided lifetime. It mostly does nothing, except signaling to the reading that the compiler is inferring a lifetime there (which it would do anyway). This one is more obscure because you can always rewrite it with some named lifetime, but the meaning changes depending on where you used it.

Where would this 'auto lifetime be used? Because there's no way to talk about "live until getting dropped" for types, since "getting dropped" is a property exclusive to values. And for values you can't annotate their lifetime anyway.

It also feels weird to me calling it 'auto, because it very far from automatic. "Always valid" is a very strong assertion to make about a reference, and except from special cases like static variables, string literals and when rvalue-static-promotion applies, you can't easily create such references. You really have to try to get one.

2 Likes

Thanks so much for your detailed explanation!

I'd like to share where I got the idea in the title from, and maybe we could discuss how the documentation could be improved for my fellow beginners to avoid the detours I took.


I ran into this problem when writing a library with a data structure like this:

pub enum Intermediate {
    SingleField(String),
    DoubleField {
        first: String,
        second: String,
    },
}

It is intended to be returned by a trait method like this:

pub trait ToIntermediate<T> {
    fn to_intermediate(&self) -> Intermediate;
}

where the trait ToIntermediate<T> would be implemented for the same type multiple times with different type parameter Ts.

Later I found out that for some implementations, the to_intermediate procedure is merely a table lookup, and the heap allocation for owned Strings is redundant since the downstream consumes the Intermediate by joining the strings together, and owning the Strings does no good.

What about changing Strings to &'static strs in enum Intermediate? But I didn't want to eliminate the possibility of returning Intermediates holding owned Strings, so I introduced a generic type parameter, and the code looked like this:

pub trait Text = Borrow<str>;

pub enum Intermediate<T: Text> {
    SingleField(T),
    DoubleField {
        first: T,
        second: T,
    },
}

pub trait ToIntermediate<T> {
    type Text: Text;
    fn to_intermediate(&self) -> Intermediate<Self::Text>;
}

Now T: Text could work with either &'static str or String.


Later on, when I was writing the function consuming the Intermediate<T> values, I needed to do an equality check on the second field of a Intermediate<T>::DoubleField variant, and hold on to the value of the field for later checks.

But I couldn't just store the borrowed &str provided by the Borrow<str> bound, as it would have been dropped on the next iteration.

Converting a borrowed &str to String works, but it does a heap allocation and clones the underlying bytes, which again is redundant, since we typically work with &'static strs and Strings, and it's enough to just copy a reference / transfer the ownership respectively.

After digging through the std library, I found that Cow<'a, str> meets the requirements perfectly, so I modified the trait alias Text into this:

pub trait Text = Borrow<str> + Into<Cow<'a, str>>;

Of course this didn't work. I deliberately started a failed build just to get some help from the compiler. (Why didn't I try to fully understand the syntaxes and mechanisms before writing the correct code by hand? Well I'd say that could be really difficult)

The compiler complained:

error[E0261]: use of undeclared lifetime name `'a`
  --> src\**
   |
** | pub trait Text = Borrow<str> + Into<Cow<'a, str>>;
   |                                         ^^ undeclared lifetime
   |
   = note: for more information on higher-ranked polymorphism, visit https://doc.rust-lang.org/nomicon/hrtb.html
help: consider making the bound lifetime-generic with a new `'a` lifetime
   |
** | pub trait Text = Borrow<str> + for<'a> Into<Cow<'a, str>>;
   |                                +++++++
help: consider introducing lifetime `'a` here
   |
** | pub trait Text<'a> = Borrow<str> + Into<Cow<'a, str>>;
   |               ++++

So I tried the first advice, as it required fewer changes to the signatures. But then the compiler complained about the implementation:

error: implementation of `From` is not general enough
  --> src\**
   |
** |     fn to_intermediate(&self) -> Intermediate<Self::Text> {
   |                                  ^^^^^^^^^^^^^^^^^^^^^^^^ implementation of `From` is not general enough
   |
   = note: `From<&'static str>` would have to be implemented for the type `Cow<'0, str>`, for any lifetime `'0`...
   = note: ...but `From<&'1 str>` is actually implemented for the type `Cow<'1, str>`, for some specific lifetime `'1`

Wait, no help?


Without knowing how to get From<&'static str> implemented for Cow<'a, str> (From<String> worked without problem), I tried the second advice (introducing lifetime parameter 'a)

After introducing a whole bunch of nastiness to the signatures, I got:

error[E0392]: parameter `'a` is never used
  --> src\**
   |
** | pub enum Intermediate<'a, T: Text<'a>> {
   |                       ^^ unused parameter
   |
   = help: consider removing `'a`, referring to it in a field, or using a marker such as `PhantomData`

But I wasn't using unsafe pointers, why should I use PhantomData? Anyway, let's give it a try, but then the compiler complained:

error[E0308]: mismatched types
  --> src\**
   |
** |     fn to_intermediate(&self) -> Intermediate<Self::Text>;
   |                                  ^^^^^^^^^^^^^^^^^^^^^^^^ lifetime mismatch
   |
   = note: expected trait `Text<'_>`
              found trait `Text<'a>`
note: the anonymous lifetime defined here...
  --> src\**
   |
** |     fn to_intermediate(&self) -> Intermediate<Self::Text>;
   |                        ^^^^^
note: ...does not necessarily outlive the lifetime `'a` as defined here
  --> src\**
   |
** | pub trait ToIntermediate<'a, T> {
   |                          ^^

And now I got really stuck.


Anyway, when I was trying random ways to rephrase my code without making the IDE complain, I fould that (after I had reverted all the changes since I added + Into<Cow<'a, str>> to the trait alias Text) I could just write:

pub trait Text = Borrow<str> + Into<Cow<'static, str>>;

And code successfully compiled.

Well, this didn't seem unnatural to me, since the only implementation of ToIntermediate<T> had an associated type Text = &'static str;

So I changed that to type Text = String (and changed the return expressions in correspondence).

Magically, the code still compiled (and ran correctly) without any problem.


So, after such a long way through “compiler hint driven coding", I finally made the observation that Into<Cow<'static, str>> is auto implemented for String.

In other words, From<String> is implemented for Cow<'static, str> even if the String itself might not have a 'static lifetime, and the documentation stated:

impl<'a> From<String> for Cow<'a, str>

instead of:

impl From<String> for Cow<'static, str>

although Cow<'static, str> is a subtype of Cow<'a, str> and could be coerced to it.

And the compiler mentioned before:

   = note: `From<&'static str>` would have to be implemented for the type `Cow<'0, str>`, for any lifetime `'0`...
   = note: ...but `From<&'1 str>` is actually implemented for the type `Cow<'1, str>`, for some specific lifetime `'1`

One would naturally think that for Into<Cow<'static, str>> to be implemented, a String with 'static lifetime would be needed which, surprisingly, is not the case.


Since I've spent so much time to figure out how to set such a simple matter straight. I couldn't help but ask myself: is that effort something necessary for a Rust beginner who just wants to treat &'static strs and Strings in a generic way?

If not, how can we improve the learning process?

Trait aliases are unstable. FYI, because of this, you almost certainly will run into edge cases where the compiler isn't as helpful as we'd like it to be. If you're a beginner and not familiar with Rust, you absolutely should not be enabling any unstable features.

If the problematic middle step had been written without a trait alias:

pub enum Intermediate<T: Borrow<str> + Into<Cow<'a, str>>> {
    SingleField(T),
    DoubleField { first: T, second: T },
}

pub trait ToIntermediate<T> {
    type Text: Borrow<str> + Into<Cow<'a, str>>;
    fn to_intermediate(&self) -> Intermediate<Self::Text>;
}

you'd get the error of

error[E0261]: use of undeclared lifetime name `'a`
 --> src/lib.rs:4:49
  |
4 | pub enum Intermediate<T: Borrow<str> + Into<Cow<'a, str>>> {
  |                                                 ^^ undeclared lifetime
  |
  = note: for more information on higher-ranked polymorphism, visit https://doc.rust-lang.org/nomicon/hrtb.html
help: consider making the bound lifetime-generic with a new `'a` lifetime
  |
4 | pub enum Intermediate<T: Borrow<str> + for<'a> Into<Cow<'a, str>>> {
  |                                        +++++++
help: consider introducing lifetime `'a` here
  |
4 | pub enum Intermediate<'a, T: Borrow<str> + Into<Cow<'a, str>>> {
  |                       +++

error[E0261]: use of undeclared lifetime name `'a`
  --> src/lib.rs:13:39
   |
13 |     type Text: Borrow<str> + Into<Cow<'a, str>>;
   |                                       ^^ undeclared lifetime
   |
help: consider making the bound lifetime-generic with a new `'a` lifetime
   |
13 |     type Text: Borrow<str> + for<'a> Into<Cow<'a, str>>;
   |                              +++++++
help: consider introducing lifetime `'a` here
   |
13 |     type Text<'a>: Borrow<str> + Into<Cow<'a, str>>;
   |              ++++
help: consider introducing lifetime `'a` here
   |
12 | pub trait ToIntermediate<'a, T> {
   |                          +++

which I think might lead to a bit more productive of an error fix cycle.


If there's a "root" issue here, it's that std provides

impl<'a> From<String> for Cow<'a, str>
// and
impl<'a> From<&'a str> for Cow<'a, str>
// but not
impl<'a> From<&'static str> for Cow<'a, str>

// without overlapping impls, it'd be spelled
impl<'short, 'long: 'short> From<&'long str> for Cow<'short, str>
// but this would cause a lot of inference breakage
// as Cow::from("str") would have multiple candidates

A general fix might be for the compiler to synthesize more general impls when variant types are involved, or at least to note this in the "lifetime not general enough" error.

I believe the miscommunication with the compiler happened somewhere around here — the error message is about the &str implementation, and not the String implementation. String satisfies the for<'a> Into<Cow<'a, str>> bound you asked for, but &str doesn't.

The reason that Cow<'static, str>: From<String> holds is that the implementation is provided for impl<'a>. This doesn't mean anything w.r.t. the String, because String doesn't have any lifetime parameters.

I find that subtyping is a horrible way of thinking about generic applicability. It's necessary for coercions, but what's useful for generics is the "outlives" relation and the "forall" binder. The impl is provided with impl<'a>, and no bounds are placed on 'a; this means that when you use the implementation, you're allowed to choose any lifetime you fancy, including 'static.

Nowadays the impl might be written as impl From<String> for Cow<'_, str> instead, which based on the explanation of your logic chain, I expect you would have intuited better. Many existing Rust devs are actually the other way, and prefer the named but unbound lifetime.

Generalizing like this is unfortunately not such a simple matter. It means requiring running headfirst into lifetime problems, and any time you're considering or the compiler starts suggesting "higher-ranked" lifetimes (for<'a>) you're venturing off anything remotely close to the "simple" path. "Lifetime not general enough" is a contender for the worst and most difficult to improve error that the compiler will emit.

The general advice is two bits:

  • Any time you're expressing a function signature of the shape fn() -> for<'a> &'a T, you want to say fn() -> &'static T instead. This holds for any "covariant" lifetime (most of them).
  • To accept either &'static str or String, don't use generics. Just use Cow<'static, str> instead. (For &'a str, Cow<'a, str>.)
1 Like

Never thought of it like this, but thanks for the info.

From my experience, the error messages you showed were practically the same as when the code were written with a trait alias.


The fact that this "virtual" implementation is effectively available for concrete types, but not trait bounds, did make me confused. Even after I got a better understanding about lifetimes, I still couldn't understand why this wasn't provided, until you mentioned "inference breakage".


You're right, I noticed that myself when writing a summary of my experience. I just wonder if other beginners might make the same mistake.

Yeah that's where I misunderstood.

Valuable note to beginners.


I found a lot of similarities between learning a new programming language and learning a foreign natural language. Without specific examples, one could easily come up with non-natively styled expressions.

For this case though, I think Cow<'static, str> works as a private consumer function parameter type, but for a public trait method that might be implemented by other developers, it requires one extra manual step of .into() and this is actually not zero-cost, if we know that only once in a while would we need to hold on to that string (where Cow<str> is needed).

(Which is why I wrote pub trait Text = Borrow<str> + Into<Cow<'static, str>>;. Otherwise, I would just need pub trait Text = Into<Cow<'static, str>>; if not directly Cow<'static, str>)

Plus, in this case the data structure would be much less compact (1~2 extra word (usize) taken by the discriminator for each occurrence of T: Text, which is a waste in those loop iterations where the value is immediately consumed and doesn't need to be preserved)

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