Why are lifetime explicit?

I was chat with a friend who is learning Rust, and he asked me why can't the compiler "guess" the lifetimes, and why do they need to be explicit. He comes from C++ where templates have exactly that properties (ie they are analyzed post-monomorphisation, and not pre-monomorphisation like Rust generics). This means that a C++ compiler would look at the body of the function and extract the lifetime information. In contrast a Rust compiler will look at the body of the function and validate that the lifetime information match what was declared in the prototype. Having pre-monomorphisation diagnostics have a very nice impact both in term of error reporting, and compilation speed. Nonetheless his question is valid. Why can't the compiler extract the lifetime information. I'm sure that I read an article somewhere that was giving an example of a lifetime that could not be inferred correctly and would result in a code code harder/impossible to use (a variable was borrowed for more time than needed).

Do you have examples of code where the compiler couldn't "guess" correctly the lifetimes.

The compiler doesn't guess the function signature, not because it can't, but we decided to not do so. In Rust we consider the function signature as a contract point between the caller and the body. And the contract is written entirely by the developer - the compiler never guess on it. It doesn't imfer the type arguments types or return type. Elided lifetimes are filled by simple mechanical rules, not something smart inference.

This gives some unique experience to the Rust developers - fearless refactoring. Since the signatures are not affected by the implementation, you can't make a breaking change regarding types and lifetime just by modifying the function body.

4 Likes

I'm curious. After many years of using C++ I have not seen that it has any notion of "lifetimes". Have I missed something with recent C++ standards?

I did recently see a presentation from cppcon about some guys at MS working on some static analysis for MSVC and Clang that could generate warnings about suspect lifetimes in programs. I have no idea how effective it is or how far it has developed. As far as I know it is not any part of the C++ standard.

Meanwhile, in my first year of Rust I don't think I have ever written a lifetime tick mark into any of my Rust code.

1 Like

This is only 99% true. Some marker traits like e.g. Send and Sync can be implicitly available for opaque function return types like -> impl Trait or for async fn’s.

1 Like

Yes, I know.And that's a really good trade-off. It's what allow pre-monomorphisation diagnostic (and more generally local reasoning). As I said "Having pre-monomorphisation diagnostics have a very nice impact both in term of error reporting, and compilation speed".

Not at all. I use the word would because it's how C++ treat other kind of generic, but it wasn't really clear.

Trait definitions need to specify lifetimes too, and they may have no code to infer from yet. Unlike C++ templates, using Rust traits in generic code gets fully type- and borrow-checked before any concrete types are substituted.

I'd say that unbounded lifetimes are the main example of this, where the most cautious programmers will go as far as defining ad-hoc functions in situ just to keep those lifetimes in check :smile:

A counter-example, however, is how the Rust trait solver currently struggles with some higher-order signatures.

  • Here is a Playground demo where you can see how the zero-cost abstraction cannot be described with generics, requiring either this "late monomorphization template-like pass" that macros are, or heap-allocated dynamic-dispatch type erasure :disappointed:
2 Likes

There's a ton of things that Rust could do that would work great on code that's already complete and correct. Much of the design choices in areas like this are to avoid introducing hazards in debugging, exploration, and similar.

My go-to example:

fn foo<T>(a: &T, b: &T) -> &T {
    todo!()
}

You really don't want the compiler saying "naw, it's fine, I looked at the body and clearly that output lifetime is disconnected from anything so you can use it however you want", because obviously at some point you want to write the body.

We do have lifetime elision so you don't need to write them by hand for common cases, but those don't look at the body. So in the comparatively-rare cases where it's not so clear which lifetimes need to get tied together, Rust intentionally asks the programmer to say what they meant.

5 Likes