Hacker News new | past | comments | ask | show | jobs | submit login
Rust Lifetimes (charlesetc.com)
102 points by luu on Oct 31, 2015 | hide | past | favorite | 83 comments



This has been my problems with Rust for the last 10-12 months.

Lifetime elision seems thoroughly broken or extremely limited in functionality, and now when I'm writing (sometimes seemingly trivial) solutions in Rust I'm spending at least as much time and mental energy explicitly annotating lifetimes as I would be if I was just managing malloc and free in C.

As a result the pain vs. benefit curve doesn't bend nearly as far toward Rust as it theoretically should.


I'm interested in seeing your use cases where lifetime elision isn't helping. I've found that it's helpful in the vast majority of cases, and when it isn't working (for functions, at least) there's a decision to be made.

It also could be a programming style thing; I've seen newcomers from C++ often trying to program in the C++ style and having lifetime troubles because the C++ style isn't really amenable to Rust's model. (I feel this might be the issue since you mentioned "sometimes seemingly trivial" -- almost every time someone has said something like that about Rust it's a matter of a programming pattern not translating directly)

Also, really, lifetimes don't do away with the effort required in manual memory management, they just confer it to compile time, so at least you can be sure that your code works and will not break in the future. They're not really a new concept, you think about them anyway in C++, just in a different way.


I'm a functional programmer (Lisp, OCaml, Erlang), and I only touch C++ when I have to wrap it in something to interface with a higher-level language.

Most of the cases where I run into this problem and end up feeling like I didn't gain much, if anything, from doing a straight C implementation are situations with deeply nested data structures.

I get that the thinking is that the benefit is that once I've made the compiler stop complaining, my memory management model should at least be sound and safe, and that is a win. Though immediately following that I start to have dream-like fantasies where this entire static analysis stage is simply bolted onto C instead of being a whole new language.

Afterall you can do some pretty impressive stuff with nothing but a pile of preprocessor macros (see Objective C).


> Most of the cases where I run into this problem and end up feeling like I didn't gain much, if anything, from doing a straight C implementation are situations with deeply nested data structures.

Your feeling doesn't line up with what has been observed in practice. Empirically, people do not get the memory management right in C. They mess up, again and again and again. In Rust, however, the compiler enforces that you get it right.

> Though immediately following that I start to have dream-like fantasies where this entire static analysis stage is simply bolted onto C instead of being a whole new language.

I don't think it's really possible to bolt Rust's semantics onto C. Too much will break: in particular C code is not written with inherited mutability, which is critical to Rust. You could maybe do something like the proposed lifetime stuff in C++, which still requires lots of annotation, enough to effectively be a whole new language.

Rust has a lot better ergonomics anyway.

> Afterall you can do some pretty impressive stuff with nothing but a pile of preprocessor macros (see Objective C).

Objective-C isn't "a pile of preprocessor macros", and it would be completely impossible to implement the lifetime system using the preprocessor.


The first versions of Objective-C were most definitely implemented as pre-processors to C compilation.


That's not the same as using the C preprocessor to implement the language.


At this year's CppCon Herb Sutter the chair of the ISO C++ committee addressed just this issue of lifetime safety. Here is a video of his talk and includes a demo of the upcoming static analysis tool that can do this with very minimal annotations https://m.youtube.com/watch?v=hEx5DNLWGgA

I remember thinking when watching the talk that this was a very bad day for Rust as C++ could now do most of what Rust was promising


The lifetime stuff in C++ is full of annotations, with effectively just as many required as Rust. I think of it effectively as a separate language. It's also significantly more complex, with points-to analysis instead of inherited mutability, and the restrictions on pointer aliasing on function entry seem very restrictive compared to what RefCell provides.

I think that Rust's system has proven itself to be about as simple and easy to use as you can get in this space. I also think that you cannot just "bolt on" a lifetime system to an existing language that was not designed for it without large amounts of annotation, modification of existing code, and additional learning curve.


The danger here to Rust is the usual "worse is better".

OS vendors will happily just add GSL and static analysis driven support to C++, with Rust becoming the language that drove them to do that and that is it.

Microsoft (GSL + static analysis, C++/CX, System C#, .NET Native) and Apple (Swift) are already working on what might be their next systems programming language.

They might lack full Rust like safety, but they are a good enough incremental approach to safety, come with integration with their existing tools and libraries.

Which leaves Rust adoption for systems programming to other OS vendors, e.g. embedded space or open source OSes.

Remember, languages are a tiny portion of the whole eco-system. Tools, libraries and community play a bigger role.


I agree. We are not starting out with a blank slate. There are already billions of lines of C++ in mission-critical production use. Companies such a Microsoft, Apple, Google, and Facebook run on C++. In addition, it has vendor support on all the major operating systems. In contrast, not even Mozilla, has bet the company on Rust (Servo is experimental and Mozilla has made no promises of actually integrating it into Firefox.) Also, Rust is not vendor supported on any platform.

So if you are a CTO or someone who has the responsibility of deciding what language to use for your performance critical software, you already have a large bias toward C++. You may be swayed toward Rust by the fact that Rust is memory safe. However, when someone comes and tells you that with this open source tool for C++ backed by the big names in C++, and developed and supported by Microsoft, and just by using the GSL types you can get most of the benefit of Rust, you will decide that the delta of improvement of Rust over C++ is not worth it.

TLDR: In programming language use pragmatic considerations trump purity. Multibillion dollar companies run on C++. With GSL and static analysis, C++ is "good enough".


That is very '90s, pre-programming-language-Renaissance thinking. If that thinking actually dominated, then you would never have seen the rise of Python, Ruby, Go, Node, Perl, PHP, Scala, Clojure, and so forth during the 2000s and 2010s. During the '90s, people predicted that Java would be the last programming language that anyone would ever use, using the same kinds of reasoning. That turned out to be false.

> You may be swayed toward Rust by the fact that Rust is memory safe. However, when someone comes and tells you that with this open source tool for C++ backed by the big names in C++, and developed and supported by Microsoft, and just by using the GSL types you can get most of the benefit of Rust, you will decide that the delta of improvement of Rust over C++ is not worth it.

Replace "Rust" with "Ruby on Rails", "C++" with "Java", and "GSL types" with "Spring", and you could make the same statement in 2005. It might even sound plausible. It would also be an incredibly wrong prediction.

> There are already billions of lines of C++ in mission-critical production use.

How many lines of memory-safe C++ are there in production use? The amount of memory-safe Rust code far exceeds that.

Ergonomics and the ecosystem matter. The fact is that the proposed static analysis tool for C++ is very restrictive and complex, from everything I've seen so far, and coupled with the annotation burden equal to that of Rust effectively makes it into a completely different language.


I agree that this might be a reason that people delay adoption, but I think things will change when the people who adopt Rust do better than the people who don't. This is similar to how things changed when people who adopted scripting languages for their web apps did better than people who didn't.


> Mozilla has made no promises of actually integrating it into Firefox.

Rust is already being integrated into Firefox


I am genuinely interested in this. Do you have any good reference about it?



Exactly! No matter the future of Rust, it has already shaken the programming community and motivated the other language designers to catch up in that domain.

As to guess whether Rust will put down C++ forever and ever, is another hard question. It will be a matter of community and sadly money... For instance, Apple's already got a much better market share with Swift (look at the job trends) simply because it's Apple. Same goes for JavaScript greatly helped by Google (V8) for their interest (a faster JavaScript engine ==> better usage of Google's web services).


> Microsoft (GSL + static analysis, C++/CX, System C#, .NET Native) and Apple (Swift) are already working on what might be their next systems programming language.

Swift at least isn't in the same space as Rust. It's a fully garbage-collected language.

> Remember, languages are a tiny portion of the whole eco-system. Tools, libraries and community play a bigger role.

And Rust has the most memory-safe low-level systems code available today. It also has Cargo and crates.io, which allow that code to be easily reused.

By contrast, existing C++ code isn't safe C++ code. It will have to be ported, often by drastically altering the idioms in use, and that takes a lot of time.


> Swift at least isn't in the same space as Rust. It's a fully garbage-collected language.

Have you spent any time reading Apple documentation?

https://developer.apple.com/swift/

"Swift is a successor to both the C and Objective-C languages."

Mesa/Cedar at Xerox PARC was also systems programming language with RC.

Going back to my comment "Remember, languages are a tiny portion of the whole eco-system. Tools, libraries and community play a bigger role.".

<devil advocate mode>

Give me Swift like Playground, Instruments integration, XCode and LLDB integration. Ability to call any Mac API the same way Swift does. Then maybe our customers will request Rust for our iDevices projects.

Give me Visual Studio integration, ability to define WinRT controls like C++/CX, create and debug COM like VC++, mixed mode debugging with .NET. Then maybe our customers will request Rust for our Windows projects.

</devil advocate mode>

Again, I am pretty aware that Rust is way better than half baked solutions like GSL + static analysis, but I have quite a few grey hairs already.

My first C++ compiler was Turbo C++ 1.0 for MS-DOS, remember when it was just released?

I also remember when C had zero presence on home computers.

Which means I was there when Ada, Modula-2, Modula-3, Oberon and derivatives were steam rolled by OS vendors betting the house in C.

As an early C++ adopter, I also carry quite a few flame war scars from being on the C++ side of the fence back on those days (vs C). As I always advocated for the C++ improved safety over what C offers.

> And Rust has the most memory-safe low-level systems code available today.

I agree, but do the OS vendors that sell the SDKs we are allowed to use, also agree or will hide their head on the sand and do another round of "worse is better" with their tools?

> By contrast, existing C++ code isn't safe C++ code. It will have to be ported, often by drastically altering the idioms in use, and that takes a lot of time.

Yes, but the eco-system is already here. Which means most companies will rather endure a slow Python 2 to Python 3 code re-factoring than move to another language.

I really want Rust to succeed and be the language I can use whenever I have to go outside JVM, .NET and mobile worlds.

However I have seen too many nice languages become victim of "worse is better" syndrome.

So sorry if my type of "heads up comment" isn't welcome.


> "Swift is a successor to both the C and Objective-C languages."

Saying something doesn't automatically make it true... however, I believe it is in this case, but maybe not to the degree the quote/you imply: it seems to me that current Swift may not be the best choice for say, writing the kernel itself, or extremely demanding components like a JS engine/the core of a web-browser.

> Remember, languages are a tiny portion of the whole eco-system. Tools, libraries and community play a bigger role

NB. the Rust leadership understands this: it's part of why crates.io was an early investment (and literally an investment: Mozilla put money into it), and why there's a pile of efforts towards improving tooling, including IDE integration.

> So sorry if my type of "heads up comment" isn't welcome.

Well... you do make a comment along these lines on essentially every Rust thread here, so it may be getting a bit repetitive at this point.


Maybe because since the mid-90's I have seen any safer alternative that I liked being steamrolled by the C and C++ duo, where the authors shared the same enthusiasm.

I will abstain from such comments from now on, hopefully Rust will succeed where the others failed.


While I'm glad to see C++ improve, and I'm happy that Rust seemed to influence this, I don't think that it will kill either Rust or C++.

As you say, there's a ton of C++ code out there. However, five things:

  1. A _lot_ of that is C++98.
  2. These rules are just proposed. There's tons of time.
  3. They don't give you as much safety as Rust.
  4. Many people have already rejected C++.
  5. Langauges don't die.

So, one and two are kind of related. We're still in the "propose" stage for all of this, which means there are still years before this would make its way into the standard. How long have things like modules taken? Who knows when the rules will be final, and when they'll actually land. You get all of this (and more) in Rust right now.

As for three, while these rules help, and are laudable, they still don't give you data race freedom. In general, they're not intended to be an ironclad safety system, just something to help catch more cases where something went wrong. See Herb's insistence that pointers should be able to dangle in some circumstances, as an example. I saw a comment on Reddit that straddles three and four, here it is: https://www.reddit.com/r/programming/comments/3m6j2c/cppcon_...

   > Just follow this set of hundreds of rules (seriously, go read the core
   > guidelines), ignore 20 years of material written about C++ before the
   > year 2011, use this non-standard third-party library of stuff we
   > really like, and you too can have a fraction of Rust's compile safety.
This is phrased a little to sarcastically imho, but there's a nugget of truth there. One of C++'s problems is that it's just got too many features, bolted on over decades. Is bolting on yet another feature the way forward? How much of that existing C++ code will even be able to take advantage of these new safety features?

A lot of people have already rejected C++, for various reasons. The details of those reasons are varied, and may be good reasons or bad reasons, but the point is, they've already said nope. A lot of people coming to Rust fit in this category. "I tried C++ one time, but it's too complex." "I can write some basic C++, but I'm not confident my code is correct." "I write Ruby code, C++ looks terrifying, and I hear it's hard never tried it." And so on. Our industry is huge, there are a lot of developers, and not all of them will use a single language even if it's 'better.' That cuts both ways.

And ties into five. I don't think the question "Will Rust kill C++" is well formed. A language that's used as much as C++ is will never die. We'll have C++ code in production for at least my lifetime, I'd bet. Rust doesn't need for C++ to go away. Languages aren't the Highlander, there can (and often are) more than one. I mean, look at Python and Ruby. They're really similar languages from a PLT perspective, but they're both active, healthy, used-by-millions languages, and many of their programmers wouldn't switch to the other. This is totally fine.


I hope that Rust succeeds in the mainstream.

If anything, please take my point of view of someone that cannot ever use Rust until the customers say I am allowed to.

On my type of work we only use first class languages, those that are provided as part of whatever platform we are targeting.

This is my point of view of "worse is better".

Many might have rejected C++ or C (e.g. I don't like C at all), but many more are on the same boat as I.

For us system programming means C++ and C, until the IT department or customer allows anything else on our dev machines.


Absolutely. This is part of why I think "Will C or C++ die" is a silly question. We're in total agreement here.


IMO "kill C++" is better replaced with "Will Rust or isocppcore be the language of choice for new C++ codebases". Where "new" includes dependencies (i.e. a project heavily dependent on an in-house C++ library used elsewhere doesn't ocunt)

I think in the majority of cases this might turn out to happen.


Also Rust has the freedom to just leave dangerous features out of the language [1]. This is something we will never be able to do with C++ without transforming it in a not backwards compatible manner.

And it's not only about safety: a language that doesn't suffer from the consequences of legacy design choices is just less mental burden, easier to learn and easier to teach.

[1] http://graydon2.dreamwidth.org/218040.html


One can simply ditch the C++ backward compatible features into an "unsafe" part of C++. That's exactly what the talk given by Herb at the CppConf is trying to address. All the old unsafe part of C++ will trigger errors or warnings. But if truly necessary one will be able to disable these protections!


> All the old unsafe part of C++ will trigger errors or warnings.

And by breaking backwards compatibility so heavily, it effectively makes the new safe C++ into a different language.


I was at the presentations, and basically other than owner<T>, and [[suppress]] I do not remember other annotations. In fact, Herb made a point that having too many anotations was a weakness of Rust, and that they wanted to not have those annotations. There were some types to use such as array_view, string_view etc, but not annotations. Perhaps you are thinking of Herb's slides where he shows things like _In_reads_(num). Those are not part of GSL. He was just contrasting the annotation heavy way Microsoft does static analysis now, with the GSL which does not have these annotations.


I'm referring mainly to [[lifetime(this)]] and friends. Those correspond directly to the lifetime annotations in Rust. Read the paper for more details.

> In fact, Herb made a point that having too many anotations was a weakness of Rust, and that they wanted to not have those annotations.

That's because Herb didn't understand Rust.

> Perhaps you are thinking of Herb's slides where he shows things like _In_reads_(num). Those are not part of GSL.

No, I'm referring to the annotations I saw in the paper: https://github.com/isocpp/CppCoreGuidelines/blob/master/docs...


  >  Herb made a point that having too many anotations was a weakness of Rust,
While he did say that, some other comments of his made it sound like he had checked out older Rust, without elision and with pointer sigils and stuff.


Check out the actual paper on the isocpp github repo. There are `[[lifetime(foo)]]` annotations.

Their proposal does include more elision than Rust (which actually leads to a loss of expressivity of patterns that arise in complex systems), but they still have plenty of annotations.


Functional idioms don't translate cleanly either :)

If you're using Rust for FFI from functional languages (or any language, really) you need to use a fair amount of unsafe code to get the interface correctly. There's not much benefit for simple things (aside from cases like these: https://gist.github.com/steveklabnik/1a3ec0ca676aaddf766e), but for more complex things it works out pretty nicely.


But there's no compile-time validation of malloc() and free()---there aren't even strict run-time checks, right, it's possible for freeing an invalid pointer to quietly corrupt memory instead of segfaulting, right?

So if these annotations took the same amount of effort as manually managing malloc() and free(), they'd still be strictly better because they're validated at compile time, no?


That can be a problem. Some common C/C++ idioms do not translate to Rust. In particular, a function which creates and returns an object is difficult to express in Rust. In Rust, you have to create the object before the call, then pass it to a function to be filled in. Either that, or go with a reference-counted type.


Rust functions return expressions, not objects and it is entirely up to the compiled code at the function call site whether it is placed in the stack or the heap, and whether the function code is inlined or not (thus expanding the stack allocated in the parent function). ::new doesn't create a structure, it just returns the expression that is supposed to initialize the struct in memory, which the compiler writes to the stack or to the heap through the Box constructor.

If you look at the antipattern example in the "Returning Pointers" section of the Rust book [1], you'll see how you are supposed to handle object creation (the box syntax is the same as Box::new) in a Rustic way

[1] https://doc.rust-lang.org/book/box-syntax-and-patterns.html


What makes you think you can't create and return an object? This is how every constructor function in rust works.


I believe this is referring to '...and keep a mutable reference to it' ie. A singleton, and many overseer style observe-and-update-on-change data binding patterns.

Basically, shared ownership is pretty central to many C++ patterns, which means they translate poorly into rust.


Are you referring to objects that contain inner pointers? Otherwise you can just return T or Box<T>.


Has "Box" settled down yet? The last time I looked, box syntax and semantics were still in flux.


Box is stable and fully supported. The built-in "box" syntax is unstable still, but Box::new suffices in almost all cases.


Yeah, at some level of complexity you do start needing some Cell/RefCell/Rc, but it's not much.


> Lifetime elision seems thoroughly broken or extremely limited in functionality

Do you have examples? Have you filed bugs?


Note that the go exemple about returning a pointer to a stack variable is actually valid in Go: variables that escape their scope are allocated on the heap.


I _think_ the author was just trying to convey that it's no longer stack allocated in this case. Escape analysis is really nice, but when you need performance, it can get in the way. I've talked to people who ended up having to leave a ton of comments with the equivalent of "dont do this, it messes up escape analysis and performance goes south" all over their codebase.


I'm actually considering adding escape analysis to _Rust_ (as a part of clippy, that is) so that we can advise users to convert heap allocated Vec/Boxes to stack allocated thingies.


That would be super cool!


struct Sheep<'c> { age: &'c i32, }

I don't get why this is needed at all and isn't implicit? What are other possible values for a lifetime in a struct?


As the person who originally pushed extremely hard for all of the lifetime elision that we have today, I agree with you that this case is still too verbose.

That said, the reason it wasn't included in the original elision RFC (https://github.com/rust-lang/rfcs/pull/141) is that the design space here is a little bit more subtle than some of the cases that did make it.

In particular, I believe that it's important to know that a particular struct contains a borrowed value somewhere.

So let's say you could leave off the `'c` on the Sheep struct, and I wrote a struct containing `Sheep`:

    struct Flock {
        sheep: Vec<Sheep>
    }
Now, we have visually lost the fact that a borrowed value is stored inside the flock. Since data structures can get very recursive, this rapidly causes useful information about ownership to get lost.

What I'd like to see personally is:

    struct Sheep<&> {
        age: &i32
    }

    struct Flock<&> {
        sheep: Vec<Sheep>
    }
What this would mean is that you can always see, by looking at a struct definition, whether it contains borrowed values (anywhere recursively), but without the grotty need to define and wire up lifetime names.

I like the fact that this maintains the strong mental notion of ownership (the meaning of the & "operator" in Rust), but, as with normal elision, it eliminates the wiring that a good compiler should be able to do for you.


There're even people that don't like the automatic moving in Rust, because they can't see where something is moved.

I can understand why people feel that way, but it's a bit irrational, because if automatic behaviour can't result into incorrect code, more ressource usage or higher work load, why should I care about?

I have pretty much the same feeling about explicit lifetimes in this example.

Again, I understand that people want the whole control, want to see every relevant information, but everything has its costs, and the relevant information in each use case isn't always the same.


We did try making moves and copies into distinct syntaxes.

  let x = foo; // <- copies
  let x = move foo; // <- moves
It was worse, or at least, was by the majority of people at the time. See http://smallcultfollowing.com/babysteps/blog/2012/10/01/move... for more of this ancient history.


Thankfully there are no move constructors in Rust, so moving something is entirely bug- and side effect free.


There's a few other configurations of lifetimes, e.g. (not meant to be exhaustive)

- only allowing storing references that are valid forever

  struct Sheep { age: &'static i32 }
- storing multiple references, with the same lifetime

  struct Sheep<'a> { age: &'a i32, name: &'a str }
or different ones

  struct Sheep<'a, 'n> { age: &'a i32, name: &'n str }
(This is usually most important for mutable references (&mut), rather than shared ones (&).)

- lifetimes that have a non-trivial relationship to other lifetimes/types, e.g. via trait bounds:

  struct Sheep<'a, N: SomeTrait<'a>> { age: &'a i32, name: N }
---

There could be some elision rule that allows omitting the explicit annotation in some cases, but I think it would be weird: allowing complete elision for the equivalent thing with types (e.g. writing `struct Foo<T> { x: T }` as just `struct Foo { x: T }`) would not be great at all IMO. These types are generic over lifetimes in essentially the same way as types that are generic over types (i.e. they can be instantiated with a lifetime (resp. type) of a downstream user's choosing), and being generic over a lifetime is part of its signature/behaviour.


Early Rust did attempt to elide here. The trouble is that if you left off <'c> then it wasn't obvious in those kinds of cases that Sheep is bound to a stack frame, which led to surprising errors. So lifetimes were made mandatory here.


In theory, Rust definitely could use extra lifetime elision. Question is how often is such elision useful.

You could have

    struct Example<'a, 'b>{ ref1: &'a i32, ref2: &'b i32}


In this case, how is the lifetime 'a different from the lifetime 'b?


More accurately, it allows the lifetimes to _be_ different.

Think of it like generics. What's the difference between:

    struct Foo<T> {x: T, y: T}
and

    struct Bar<T,U> {x: T, y: U}
"How's T different from U"? That's not the right question to ask -- over here Bar lets T be different from U, whereas in Foo both x and y must have the same type.

With the example above

    struct Example<'a, 'b>{ ref1: &'a i32, ref2: &'b i32}
this means that there are two references of independent lifetimes. It could also be:

    struct Example<'a>{ ref1: &'a i32, ref2: &'a i32}
which means that the lifetimes must match, or even

    struct Example<'a, 'b: 'a>{ ref1: &'a i32, ref2: &'b i32}
which means that 'b is a subset of 'a. (This becomes useful if a struct stores a borrow to a thing and then a borrow of something extracted from that thing)

While the first case is the most common (and theoretically could be elided), it's still often a tossup and it's clearer when you know that a struct is borrowing things.


Aren't there subtype coercions that make this syntax useful only rarely? (something like: if 'b: 'a, then &'a and &'b will both be treated as 'b for this instance, or maybe the other way around? And with only lexical lifetimes, for two references visible in the same scope, one is necessarily a subtype of the other, no?)

The only time I've ever used multiple explicit lifetimes, I was accidentally trying to trick the compiler into accepting shared mutability. The compiler was not fooled.


Yes, you're correct. You need this syntax when the borrow checker forces you to add this dependency, i.e. when you're stuffing one reference inside another, or swapping references, or whatever.

This is why elision is so awesome, it takes care of almost all the cases where you need a "regular" lifetime specification on a function. When elision doesn't works usually there is something you need to specify.

The subtyping works for callers; lifetimes get stretched and sqeezed to fit the function signature. But the function body itself won't compile when you're doing things like stuffing references into other references unless the lifetimes are right.


Thank you. The article made it sound to me like struct Foo<'c>, 'c is the lifetime of the struct. Am I correct in thinking that, in reality, 'c must live as long or outlive the struct?


Yeah, the lifetimes on a struct are the lifetimes of things that the struct points to, not an instance of the struct itself. For an instance to be valid, everything it points to must be valid, so if `foo: Foo<'c>`, `foo` cannot be stored for longer than `'c`. E.g. this code is invalid, because the `Foo` instance would live longer than the thing it points to:

  let foo = {
      let age = 0;
      Foo { age: &age }
  };
The `age` variable goes out of scope at the } (i.e. the maximum possible lifetime for &age ends there), and hence letting the `Foo` escape from that scope would be a bad idea.

(NB. this applies to the "core" lifetime'd types like `&'c T` too: that is really just a nicer way to write a parameterised type like `Ref<'c, T>`. So the rule "cannot return a reference to a local variable" is the same rule that stops a `Foo<'c>` living too long.)


Makes perfect sense. Thank you!


The article is wrong/misguided. See my comment above :)

When you say `Foo<'c>`, all you're saying is that each `Foo` instance has an associated lifetime `'c`, similar to a type parameter. When you add the `x: &'c u8` field, then you're saying that "this lifetime is the lifetime of the inner u8".

> Am I correct in thinking that, in reality, 'c must live as long or outlive the struct

For the specific example, yes. It's possible to have an example where 'c must be shorter lived, and cannot outlive the struct.

If you had an &mut instead of & there, then 'c must live exactly as long as the struct.


Thank you.

> It's possible to have an example where 'c must be shorter lived, and cannot outlive the struct.

I'm interested in such an example. Could you provide one?


Actually, I just looked into it, that's no longer possible. I was talking about contravariant lifetime positions, which happen with functions, but now it seems like function pointers are invariant in those positions, not contravariant.

The idea was to have something like

    struct Foo<'a> {f: fn(&'a u8)}
should only be able to accept pointers that live shorter than the struct. But contravariance seems to have been removed.


Makes sense. I can see how having contravariant function arguments could produce some surprising behavior.


I think it is intentional. Rust wants you to be explicit about lifetimes in type definitions.


cool post. what about traits and 'static?

Anyhow; Its definitely worth making two points about lifetimes very clear:

1) They don't exist at runtime.

Lifetimes are purely a compile time thing the compiler uses for formal safety.

2) 'a is not a actual lifetime, it is the minimum possible life time valid for that item; ie. a bound, not a concrete value.

&'a means; this arg/trait/ref/whatever must live at least as long as 'a. That's why different objects with different lifetimes can coexist in a struct of 'a, without needing 'a, 'b, 'c etc for each reference in the struct.


It's not clear from context in your comment if you're saying this, but, for `x: &'a T`, 'a is a bound in two ways:

- it is the maximum possible lifetime of the x variable itself,

- it is the minimum lifetime of the `T` instance x points to.

These are 100% related of course (x can't point to something that is invalid), but it is worth being careful about which end of a reference/pointer you're talking about when talking about validity.


Thanks for this comment, that clarifies things a lot for me. I think it should be in the docs (with equal brevity) if not already.


I've made a note to check it out, thanks.


  Rust is a unique language in that it deallocates memory on the heap
  without requiring the writer to call free,
  while at the same time having no need for a garbage collector.
Someone correct me if I'm wrong but surely this is not unique - don't Objective C and Swift have automatic reference counting which accomplishes much the same thing?


Intersection ARC, Lifetimes:

* Not having to worry about trivial memory/resource allocations

ARC - Lifetimes:

* Can blow up hilariously with refcount cycles

* Incurs a runtime overhead

Lifetimes - ARC:

* For the subset of allocation patterns it handles, it does so with zero runtime overhead (i.e. invalid programs do not compile, invalid = leaks memory)

* Use extends beyond just memory allocation/deallocation (see the concurrency docs [1] for example) (note that Mozilla wrote Rust to help write Servo [2], their prototype highly-parallel browser engine, so this use case was a target from the getgo)

* Cannot encode all data structures a GC can (hence why reference counting is part of the stdlib [3])

[1]: https://doc.rust-lang.org/book/concurrency.html

[2]: https://github.com/servo/servo

[3]: https://doc.rust-lang.org/alloc/rc/


> invalid = leaks memory

Not quite true- invalid use of lifetimes typically means use-after-free rather than a memory leak (and if you include the standard library, then you can leak memory in safe Rust).


Its interesting to me how everyone goes to "memory leaks" as their example of memory unsafety, and then if we're talking about Rust we need to clarify that Rust allows leaks (but it makes them hard to do by accident). I guess mnemonically "memory leak" is easier to bring to mind than "dangling pointer" when talking about memory.


It's also funny because memory leaks literally aren't a memory safety issue. If they were, every garbage collected language would be memory-unsafe.


Lots of people consider refcounting as "garbage collection". Not everyone, but many people.


Of course it's not unique, that's a core feature of C++.


[This comment is outdated, I discussed this with the author and he improved the post :)]

I feel this post confuses two different notions of lifetime. And also mixes concrete lifetimes with lifetime parameters.

There's the CS notion of liveness, and then there's the lifetime notation in Rust.

> To know when to deallocate objects

This is handled by the CS notion of liveness. Well, an approximation to it. "Things are live until they are used, or until they can no longer be used".

The deallocation comes from Rust's affine type system.

In fact, Box<T> and other self-deallocating things don't have an associated "lifetime" in the Rust sense of the word, at least not one that dictates their deallocation.

The lifetime syntax is basically a form of generics which lets one put constraints on the allowed liveness of &-pointers and their ilk.

> example_function<'a> names the lifetime defined by this function, called 'a.

This isn't true. There does exist an anonymous inner lifetime for the scope of the function. It's not `'a`

What's happening here is that we're saying, "For any lifetime `'a`, we can have a version of example_function which returns a pointer which can live that long.". This doesn't compile, because the compiler knows that this function can only return pointers which live as long as its scope or less, not "any" lifetime. (Which is impossible to return from anyway).

This becomes clearer when you have a function like `fn foo<'a> (x: &'a [u8]) -> &'a u8 { &x[0] }` (FWIW if you omit the lifetimes Rust will elide them and internally produce the same function). Here, what we're saying is, "For any lifetime 'a, foo can take a slice that lives as long as 'a and return a pointer that lives as long as 'a". This compiles, because the compiler can see that a reference to x will live as long as x itself.

Similarly, with structs:

> Sheep<'c> instead of Sheep > > This is not doing anything concrete. It's just giving a name to the lifetime that Sheep structs can last for.

This is wrong, too. It's giving a _parameter_ to Sheep, saying "Every Sheep has an associated lifetime 'c, which is the lifetime of its inner reference".

In fact, it is possible to define the body of sheep in such a way that Sheep is guaranteed to live _longer_ than `'c` (instead of shorter, as in the example above). This is explained in the Rustonomicon (http://doc.rust-lang.org/stable/nomicon/subtyping.html). But it's an advanced topic, doesn't crop up often in actual Rust programming.


> Here, what we're saying is, "For any lifetime 'a, foo can take a slice that lives as long as 'a and return a pointer that lives as long as 'a".

Another way to phrase it is: the return value of the function is guaranteed to be valid for at least as long as the argument (but may borrow from it, and hence mutations of the argument need to be outlawed as long as the return value is held).


Agreed, it's nicer to talk about "validity" than "liveness" :)


Well, my rephrasing was meant to be more than just s/validity/liveness/: I find that removing quantifiers (like for-all) often improves my understanding, as it usually gives more obvious context/motivation for what the goal is. It is then easier to understand the more formal version once I know approximately what it is trying to do.


Can someone explain what happened in the C example?


Well, it's not guaranteed to print 3, it can do anything it wants. Since you have a dangling pointer, it's not pointing to valid memory. So it depends.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: