As a Rust beginner I read lifetimes backwards, thinking <'a> means I'm "declaring a lifetime" which I then use. What that actually declares is a placeholder for a lifetime the compiler will attempt to find wherever that struct or function is used, just as it would attempt to find a valid type for a type generic <T> at the points of usage.
Once I fixed that misconception everything made much more sense. Reminding myself that only the function signature matters, not the actual code, was the other thing I needed to really internalize.
The compiler messages hinder this sometimes, as when the compiler says "X doesn't live long enough" it actually means "using my limited and ever-evolving ability to infer possible lifetimes from your code, I can't find one that I can use here".
This is also (for me, anyway) a common "it's fine but it won't compile" case, where you don't have enough lifetime parameters. In other words, you're accidentally giving two things the same lifetime parameter when it's not actually necessary to require that the compiler come up with a single lifetime that works for both. The compiler error for that does not typically lead you to a solution directly.
fn pick_first<'a>(x: &'a str, y: &'a str) -> &'a str {
x // We only actually return x, never y
}
fn main() {
let s1 = String::from("long-lived");
let result;
{
let s2 = String::from("short-lived");
result = pick_first(&s1, &s2);
} // s2 dropped here
println!("{}", result);
}
The error here is "borrowed value [pointing to &s2] does not live long enough". Of course it does live long enough, it's just that the constraints in the function signature don't say this usage is valid.Thinking as a beginner, I think part of the problem here is the compiler is overstating its case. With experience, one learns to read this message as "borrowed value could not be proved to live as long as required by the function declaration", but that's not what it says! It asserts that the value in fact does not live long enough, which is clearly not true.
(Edit: having said this, I now realize the short version confuses beginners because of the definition of “enough”. They read it as “does not live long enough to be safe”, which the compiler is not—and cannot be—definitively saying.)
When this happens in a more complex situation (say, involving a deeper call tree and struct member lifetimes as well), you just get this same basic message, and finding the place where you've unnecessarily tied two lifetimes together can be a bit of a hunt.
My impression is that it's difficult or impossible for the compiler to "explain its reasoning" in a more complex case (I made an example at [0] [1]), which is understandable, but it does mean you always get this bare assertion "does not live long enough" and have to work through the tree of definitions yourself to find the bad constraint.
[0] https://play.rust-lang.org/?version=stable&mode=debug&editio...
[1] https://play.rust-lang.org/?version=stable&mode=debug&editio...
error[E0597]: `buffer` does not live long enough
--> src/main.rs:63:21
|
59 | let report = {
| ------ borrow later stored here
60 | let mut buffer = Vec::new();
| ---------- binding `buffer` declared here
61 | let ctx = Context {
62 | config: &config,
| ------ this field and `buffer` are required by `Context` to have the same lifetime
63 | buffer: &mut buffer,
| ^^^^^^^^^^^ borrowed value does not live long enough
...
68 | };
| - `buffer` dropped here while still borrowed
help: consider making different fields in `Context` have independent lifetimes
|
4 | struct Context<'a> {
| ^^
5 | config: &'a Config,
| ^^
6 | buffer: &'a mut Vec<u8>,
| ^^
7 | }Rust tutorials introduce lifetimes and stick them into the brackets, but I don’t remember one that explains genericity in a general way first, then applies it to both types and lifetimes. Lifetimes come across as a special thing that happens to be in the same brackets. (Or maybe that was just me!)
I believe the model in the compiler isn’t quite so pure in reality (IIRC, struct lifetimes can affect type unification), but most of the time this is the easiest way to understand it.
This was my biggest problem when I used to write Rust. The article has a small example but when you start working on large codebases these problems pop up more frequently.
Everyone says the Rust compiler will save you from bugs like this but as the article shows you can compile bugs into your codebase and when you finally get an unrelated error you have to debug all the bugs in your code. Even the ones that were working previously.
> Rust does not know more about the semantics of your program than you do
Also this. Some people absolutely refuse to believe it though.
However, you've still got to do that job of encoding the semantics. Moreover, the default semantics may not necessarily be the semantics you are interested in. So you need to understand the default semantics enough to know when you need something different. This is the big disadvantage of lifetime elision: in most cases it works well, but it creates defaults that may not be what you're after.
The other side is that sometimes the semantics you want to encode can't be expressed in the type system, either because the type system explicitly disallows them, or because it doesn't comprehend them. At this point you start running into issues like disjoint borrows, where you know two attributes in a struct can be borrowed independently, but it's very difficult to express this to the compiler.
That said, I think Rust gives you more power to express semantics in the type system than a lot of other languages (particularly a lot of more mainstream languages) which I think is what gives rise to this idea that "if it compiles, it works". The more you express, the more likely that statement is to be true, although the more you need to check that what you've expressed does match the semantics you're aiming for.
Of course, if your program compiles, that doesn't mean the logic is correct. However, if your program compiles _and_ the logic is correct, there's a high likelihood that your program won't crash (provided you handle errors and such, you cannot trust data coming from outside, allocations to always work, etc). In Rust's case, this means that the compiler is much more restrictive, exhaustive and pedantic than others like C's and C++'s.
In those languages, correct logic and getting the program to compile doesn't guarantee you are free from data races or segmentation faults.
Also, Rust's type system being so strong, it allows you to encode so many invariants that it makes implementing the correct logic easier (although not simpler).
That is one hell of a copium disclaimer. "If you hold it right..."
Cloudflare used a tool, broke parts of the internet.
Citation needed.
I don't believe that it's guaranteed in Rust either, despite much marketing to the contrary. It just doesn't sound appealing to say "somewhat reduces many common problems" lol
>Also, Rust's type system being so strong, it allows you to encode so many invariants that it makes implementing the correct logic easier (although not simpler).
C++ has a strong type system too, probably fancier than Rust's or at least similar. Most people do not want to write complex type system constraints. I'm guessing that at most 25% of C++ codebases at most use complex templates with recursive templates, traits, concepts, `requires`, etc.
Some of the things you can do, often with a wild amount of boilerplate (tagged unions, niches, etc.), and some of the things are fundamentally impossible (movable non-null owning references).
C++ templates are more powerful than Rust generics, but the available tools in Rust are more sophisticated.
If you aren't instantiating a template, then it isn't used, so who cares if it has theoretical errors to be figured out later? This behavior is in fact used to decide between alternative template specializations for the same template. Concepts do it better in some ways.
Just because you aren't instantiating a template a particular way doesn't necessarily mean no one is instantiating a template a particular way.
A big concern here would be accidentally depending on something that isn't declared in the concept, which can result in a downstream consumer who otherwise satisfies the concept being unable to use the template. You also don't get nicer error messages in these cases since as far as concepts are concerned nothing is wrong.
It's a tradeoff, as usual. You get more flexibility but get fewer guarantees in return.
>Just because you aren't instantiating a template a particular way doesn't necessarily mean no one is instantiating a template a particular way.
What I meant is, if the thing is not instantiated then it is not used. Whoever does come up with a unique instantiation could find new bugs, but I don't see a way to avoid that. Likewise someone could just superficially meet the concept requirements to make it compile, and not actually implement the things they ought to. But that's not a problem with the language.
I suppose it depends on how much faith you place in the foresight of whoever is writing the template as well as their vigilance :P
As a fun (?) bit of trivia that is only tangentially related: one benefit of definition-site checking is that it can allow templates to be separately compiled. IIRC Swift takes advantage of this (polymorphic generics by default with optional monomorphization) and the Rust devs are also looking into it (albeit the other way around).
> Whoever does come up with a unique instantiation could find new bugs, but I don't see a way to avoid that.
I believe you can't avoid it in C++ without pretty significant backwards compatibility questions/issues. It's part of the reason that feature was dropped from the original concepts design.
> Likewise someone could just superficially meet the concept requirements to make it compile, and not actually implement the things they ought to.
Not always, I think? For example, if you accidentally assume the presence of a copy constructor/assignment operator and someone else later tries to use your template with a non-copyable type it may not be realistic for the user to change their type to make it work with your template.
The actual effects depend on a lot of things. I'm just saying, it seems contrived to me, and the most likely outcome of this type of broken template is failed compilation.
>As a fun (?) bit of trivia that is only tangentially related: one benefit of definition-site checking is that it can allow templates to be separately compiled.
This is incompatible with how C++ templates work. There are methods to separately compile much of a template. If concepts could be made into concrete classes and used without direct inheritance, it might work. But this would require runtime concepts checking I think. I've never tried to dynamic_cast to a concepts type, but that would essentially be required to do it well. In practice, you can still do this without concepts by making mixins and concrete classes. It kinda sucks to have to use more inheritance sometimes, but I think one can easily design a program to avoid these problems.
>I believe you can't avoid it in C++ without pretty significant backwards compatibility questions/issues. It's part of the reason that feature was dropped from the original concepts design.
This sounds wrong to me. Template parameters plus template code actually turns into real code. Until you actually pass in some concrete parameters to instantiate, you can't test anything. That's what I mean by saying it's "unavoidable". No language I can dream of that has generics could do any different.
>Not always, I think? For example, if you accidentally assume the presence of a copy constructor/assignment operator and someone else later tries to use your template with a non-copyable type it may not be realistic for the user to change their type to make it work with your template.
I wasn't prescribing a fix. I was describing a new type of error that can't be detected automatically (and which it would not be reasonable for a language to try to detect). If the template requires `foo()` and you just create an empty function that does not satisfy the semantic intent of the thing, you will make something compile but may not actually make it work.
Sure. Contrivance is in the eye of the beholder for this kind of thing, I think.
> and the most likely outcome of this type of broken template is failed compilation.
I don't think that was ever in question? It's "just" a matter of when/where said failure occurs.
> This is incompatible with how C++ templates work.
Right, hence "tangentially related". I didn't mean to imply that the aside is applicable to C++ templates, even if it could hypothetically be. Just thought it was a neat capability.
> This sounds wrong to me.
Wrong how? Definition checking was undeniably part of the original C++0x concepts proposal [0]. As for some reasons for its later removal, from Stroustrup [1]:
> [W]e very deliberately decided not to include [template definition checking using concepts] in the initial concept design:
> [Snip of other points weighing against adding definition checking]
> By checking definitions, we would complicate transition from older, unconstrained code to concept-based templates.
> [Snip of one more point]
> The last two points are crucial:
> A typical template calls other templates in its implementation. Unless a template using concepts can call a template from a library that does not, a library with the concepts cannot use an older library before that library has been modernized. That’s a serious problem, especially when the two libraries are developed, maintained, and used by more than one organization. Gradual adoption of concepts is essential in many code bases.
And Andrew Sutton [2]:
> The design for C++20 is the full design. Part of that design was to ensure that definition checking could be added later, which we did. There was never a guarantee that definition checking would be added later.
> To do that, you would need to bring a paper to EWG and convince that group that it's the right thing to do, despite all the ways it's going to break existing code, hurt migration to constrained templates, and make generic programming even more difficult.
I probably could have used a more precise term than "backwards compatibility", to be fair.
> Until you actually pass in some concrete parameters to instantiate, you can't test anything. That's what I mean by saying it's "unavoidable".
I'm a bit worried I'm misunderstanding you here? It's true that C++ as it is now requires you to instantiate templates to test anything, but what I was trying to say is that changing the language to avoid that requirement runs into migration/backwards compatibility concerns.
> No language I can dream of that has generics could do any different.
I've mentioned Swift and Rust already as languages with generics and definition-site checking. C# is another example, I believe. Do those not count?
> I wasn't prescribing a fix. I was describing a new type of error that can't be detected automatically (and which it would not be reasonable for a language to try to detect). If the template requires `foo()` and you just create an empty function that does not satisfy the semantic intent of the thing, you will make something compile but may not actually make it work.
My apologies for the misdirected focus.
In any case, that type of error might be "new" in the context of the conversation so far, but it's not "new" in the PL sense since that's basically Rice's theorem in a nutshell. No real way around it beyond lifting semantics into syntax, which of course comes with its own tradeoffs.
[0]: https://isocpp.org/wiki/faq/cpp0x-concepts-history#cpp0x-con...
[1]: https://www.stroustrup.com/good_concepts.pdf
[2]: https://old.reddit.com/r/cpp/comments/cx141j/c20_concepts_an...
>I'm a bit worried I'm misunderstanding you here? It's true that C++ as it is now requires you to instantiate templates to test anything, but what I was trying to say is that changing the language to avoid that requirement runs into migration/backwards compatibility concerns.
I see now. I could imagine a world where templates are compiled separately and there is essentially duck typing built into the runtime. For example, if the template parameter type is a concept, your type could be automatically hooked up as if it was just a normal class and you inherited from it. If we had reflection, I think this could also be worked out at compile time somehow. But I'm not very up to speed with what has been tried in this space. I'm guessing that concept definitions can be very extensive and also depend on complex expressions. That sounds hairy compared to what could be done without concepts, for example with an abstract class.
I don't think so? From my understanding what you can do with concepts isn't much different from what you can do with SFINAE. It (primarily?) just allows for friendlier diagnostics further up in the call chain.
I seem to have struck a nerve with my post, which got 4 downvotes so far. Just for saying Rust is not actually better than C++ in this one regard lol.
This isn’t about tooling, it’s about language features and type systems.
It's just that once it compiles Rust code will work more often than most languages, but that doesn't mean Rust code will automatically be bug free and I don't think anyone believes that.
Could've been clearer above.
This is also something that Rust does protect against in safe code, by requiring initialization of all memory before use, or using MaybeUninit for buffers that aren't, where reading the buffer or asserting that it has been initialized is an unsafe operation.
- https://bsky.app/profile/codewright.bsky.social/post/3m4m5mv...
- https://bsky.app/profile/naps62.bsky.social/post/3lpopqwznfs...
If Rust code compiles, it probably has a lower defect rate than corresponding code written by the same team in another language, all else being equal.
But I don't doubt there are people who take that phrase too literally, though.
But I have not seen any resource or anyone making technical points ever assert that the Rust compiler can verify program logic. That doesn't even make sense - the compiler isn't an AI that knows your intentions. Everybody is always clear that it only verifies memory safety.
Now regarding the 'most of the time' part. The part below is based purely on my experience and your mileage may vary. It's certainly possible to compile Rust programs with logical/semantic errors. I have made plenty. But the nature of C/C++ or similar manually memory-managed languages is such that you can make memory safety bugs quiet easily and miss them entirely. They also stay hidden longer.
And while logical errors are also possible, most people write and test code in chunks of sizes small enough where they feel confident enough to understand and analyze it entirely within their mind. Thus they tend to get caught and eliminated earlier than the memory safety bugs.
Now since Rust handles the memory safety bugs for you and you're reasonably good at dealing with logical bugs, the final integrated code tends to be bug-free, surprisingly more often than in other languages - but not every time.
There is another effect that makes Rust programs relatively more bug-free. This time, It's about the design of the code. Regular safe Rust, without any runtime features (like Rc, Arc, RefCell, Mutex, etc) is extremely restrictive in what designs it accepts. It accepts data structures that have a clear tree hierarchy, and thus a single-owner pattern. But once you get into stuff like cyclic references, mutual references, self references, etc, Rust will simply reject your code even if it can be proven to be correct at compile time. You have three options in that case: Use runtime safety checks (Rc, RefCell, Mutex, etc. This is slightly slower) OR use unsafe block and verify it manually, OR use a library that does the previous one for you.
Most of the code we write can be expressed in the restricted form that safe Rust allows without runtime checks. So whenever I face such issues, my immediate effort is to refactor the code in such way. I reach for the other three methods only if this is not possible - and that's actually rare. The big advantage of this method is that such designs are relatively free of the vast number of logical bugs you can make with a non-tree/cyclic ownership hierarchy. (Runtime checks convert memory safety bugs into logical bugs. If you make a mistake there, the program will panic at runtime.) Therefore, the refactored design ends up very elegant and bug-free much more often than in other languages.
I think is is a fairly bad example to pick, because the fact that the person says “I can basically think” and “most of the time it does” (emphasis mine) shows that they don't actually believes it will makes bug-free programs.
They are just saying that “most of the time” the compiler is very very helpful (I agree with them on that).
Who says this? I've never seen someone argue it makes it impossible to write incorrect code. If that were the case then there's no reason for it to have an integrated unit testing system. That would be an absurd statement to make, even if you can encode the entire program spec into the type system, there's always the possibly the description of a solution is not aligned with the problem being solved.
Rust programs can't know what you want to do, period.
https://qouteall.fun/qouteall-blog/2025/How%20to%20Avoid%20F...
Soundness does not cover semantic correctness. Maybe you want to wipe $HOME.
It depends on what you mean by "freed". Can one write a custom allocator in Rust? How does one handle reading from special addresses that represent hardware? In both of these scenarios, one might read from or write to memory that is not obviously allocated.
#![feature(closure_lifetime_binder)]
fn main() {
let identity = for<'a> |x: &'a i32| -> &'a i32 { x };
}It just means that it could live until the end of the program and that case should be considered when dealing with it, there's no guarantee that it will drop earlier. But it may drop at any time, as long as there are no remaining references to it, it does not need to be in memory forever.
It's a subtle distinction and it is easy to misinterpret. For instance Tokio tasks are 'static and it felt wrong initially because I thought it would never drop them and leak memory. But it just means that it doesn't know when they will be dropped and it cannot make any promises about it., that's all.
Contagious borrow issue is a common problem for beginners.
> Others think someone from the Rust (programming language, not video game) development community was responsible due to how critical René has been of that project, but those claims are entirely unsubstantiated.
What is this culture war you're fighting?