Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Q&A

Welcome to Software Development on Codidact!

Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.

What allows a string slice (&str) to outlive its scope?

+8
−0

As a relative newcomer to Rust, I'm trying to understand the behaviour of lifetimes, but I am confused by the following code:

let s: &str = "first";
let mut r: &str = s;
println!("First ref is {}", r);
{
    let inner: &str = "second";
    r = inner;
}
println!("Second ref is {}", r);

Since we are taking a long-lived reference r to a string slice inner which is destroyed at the end of its scope, I was expecting this code to fail to compile with a "variable does not live long enough" error. But to my surprise, it compiles fine, and prints valid output:

First ref is first
Second ref is second

It seems that the reference r is somehow keeping the "second" string slice alive beyond the end of the scope in which it is defined, but I haven't seen anything in the book which mentions this. What am I missing?

History
Why does this post require moderator attention?
You might want to add some details to your flag.
Why should this post be closed?

0 comment threads

2 answers

+8
−0

tl;dr, the lifetime of "second" is static

The heart of your confusion is this:

Since we are taking a long-lived reference r to a string slice inner which is destroyed at the end of its scope,

inner is gone at the end of its scope, yes. However, its value still lives on.

We have to think about the difference between values and variables. r doesn't reference inner, it references "second". What's probably confusing you is that inner has a reference as its value, and the lifetime of the reference is not the same as the lifetime of the referenced value.

r = inner;

This performs a copy of the value of inner. We are not referencing inner. Now, what is the value of inner? "second" is a string literal. It has a lifetime of static. Therefore, inner has a value of type &'static str.

After the assignment, the value of r thus also a &'static str. Indeed, we can actually just specify this and confirm that this compiles cleanly:

let s: &'static str = "first";
let mut r: &'static str = s;
println!("First ref is {}", r);
{
    let inner: &'static str = "second";
    r = inner;
}
println!("Second ref is {}", r);

If this still confuses you, let's work through a different example:

let s = 1;
let mut r = s;
println!("First r is {}", r);
{
    let inner = 2;
    r = inner;
}
println!("Second r is {}", r);

This should much more clearly show what's going on. We're just passing around values; who cares that inner dies at the end of its scope.


I said at the beginning that

r doesn't reference inner, it references "second".

What would r referencing inner look like? Well, we'd borrow it instead of copying its value. (We also have to borrow s for parity)

let s: &str = "first";
let mut r: &&str = &s;
println!("First ref is {}", r);
{
    let inner: &str = "second";
    r = &inner;
}
println!("Second ref is {}", r); // !!!!!
  |
7 |     r = &inner;
  |         ^^^^^^ borrowed value does not live long enough
8 | }
  | - `inner` dropped here while still borrowed
9 | println!("Second ref is {}", r); // !!!!!
  |                              - borrow later used here

As expected!


This is getting kind of long, but on a final note, look at how this introduces a double reference. Let's think about lifetimes!

let s: &str = "first"; // &'static str
let mut r: &&str = &s; // &'<s> &'static str
println!("First ref is {}", r);
{
    let inner: &str = "second"; // &'static str
    r = &inner;                 // &'<inner> &'static str
}
println!("Second ref is {}", r); // !!!!!

For this purpose, '<s> means "has the same lifetime as s".

This illustrates the point I made earlier: The lifetime of the reference is not the same as the lifetime of the referenced. At r = &inner;, we can see that we have those two lifetimes, the lifetime of inner and the lifetime of "second", and those two are not the same.

History
Why does this post require moderator attention?
You might want to add some details to your flag.

0 comment threads

+3
−0

There are two things going on here. One which technically explains what's going on fully, and another potential misconception you have.

For the former, &T implements the Copy trait regardless of what T is. So all that's happening is when r = inner; executes it just does a bitwise copy of the reference which inner is over r. It's no different than if r and inner had been declared as i64 say. Indeed, you can verify that a move didn't happen by adding code that uses inner after the r = inner; line. inner (trivially) owned a reference to a str but it never owned the str itself.

The potential misconception is occurrences of string literals don't mean to dynamically allocate (either on the heap or the stack) a string, and then return a reference to it. Instead, the compiler will statically allocate space for the string literal and all occurrences of the string literal will be replaced with references to this statically allocated memory whose lifetime is the whole program. Formally, this is represented by the 'static lifetime which subsumes, i.e. outlives, all other lifetimes.

Incidentally, there is some terminological complexity here. The reference that inner is bound to has a lifetime which, in this example, is the scope containing inner. Reference types also contain a lifetime, the 'a in &'a T, and that is how long the reference type requires the referred to data to live. Finally, the actual lifetime of the referred to data can be any lifetime which subsumes the lifetime the reference requires, i.e. a &'a T reference can refer to data that has a lifetime of 'b as long as 'a : 'b, i.e. 'b contains 'a.

In summary, inner is a short-lived reference to immortal data. s and r are slightly longer-lived references to immortal data. r = inner; is just a bitwise copy of a pointer, and this is safe because the referred to data, being immortal will outlive any lifetime the reference type requires, i.e. 'a : 'static for all 'a.

Playing with the following type can help illustrate what's happening.

#[derive(Debug)]
struct NoisyDrop {
    i: i64
}
impl Drop for NoisyDrop {
    fn drop(&mut self) {
        println!("Dropping {}", self.i) 
    }
}

Since NoisyDrop does not implement the Copy trait, no implicit copying will happen.

This first example illustrates what you were expecting, except now that we are locally allocating data, it behaves as you expect.

let s = &NoisyDrop { i: 1 };
let mut r = s;
println!("First ref is {:?}", r);
{
    let inner = &NoisyDrop { i: 2 };
    r = inner;
}
println!("Second ref is {:?}", r);

This will fail to compile since NoisyDrop { i: 2 } doesn't live as long as r's type requires. However, what is actually happening in your example is more like:

let s = &NoisyDrop { i: 1 };
let t = &NoisyDrop { i: 2 };
let mut r = s;
println!("First ref is {:?}", r);
{
    let inner = t;
    r = inner;
}
println!("Second ref is {:?}", r);

which is unproblematic.

Not directly related to your question, but you may find the output of this final example interesting:

let s = NoisyDrop { i: 1 };
let mut r = s;
println!("First ref is {:?}", r);
{
    let inner = NoisyDrop { i: 2 };
    println!("Before assign");
    r = inner; 
    println!("After assign");
}
println!("Second ref is {:?}", r);

Here we're actually performing moves of the underlying data which changes who's responsible for calling drop on the data. This allows data to outlive the scope in which it is allocated. Note that in this example, unlike the others, inner is no longer usable after r = inner;.

History
Why does this post require moderator attention?
You might want to add some details to your flag.

0 comment threads

Sign up to answer this question »