Returning a new slice from a function?

How is it possible that this program works?

fn main() {
    let msg = "I HATE bananas";

    println!("The value returned from the function is {}.", get_s(&msg));
    println!("{}", msg);
}

fn get_s(m: &str) -> &str { 
    let q = "I LOVE Jennifer";
    println!("{}", m);
    q
}

My understanding is that within the function, ‘q’ is a brand new slice of a string literal, and once the function exits, that string literal and its slice should cease to exist.

If I was returning ‘m’, I could understand, because the underlying String of which ‘m’ is a slice, remains in existence even when the function exits.

It “feels” like the function is seeing that a slice is incoming, so it expects to be able to return a slice on that same underlying String, but then gets confused and creates a new slice unrelated to that original underlying String and returns that slice even though that slice isn’t borrowed from the original String but rather from a now-no-longer existing string literal.

I don’t understand how this can be.

Hi there, great question.

The reason this works at all is because the string slice you’re creating in the function has a static lifetime. "I LOVE Jennifer" is embedded in the progam binary, so it never “ceases to exists” for the purposes of the program.

But there is more to it than that! Let’s assume a function like this:

fn get_s() -> &str { 
    let q = "I LOVE Jennifer";
    q
}

This is the same function as yours, except I removed the input argument. Now the compiler complains:

missing lifetime specifier
this function’s return type contains a borrowed value, but there is no value for it to be borrowed from

(I don’t know how much you already learned about lifetimes, but I will assume some knowledge so this doesn’t get too long. Just ask if something’s unclear.)

The problem is that just by looking at the function signature, a user of the function or the compiler have no idea how long that string will live:

fn get_s() -> &str {}

The “normal” way to fix this is to tell the compiler the correct lifetime, which is 'static it is a special lifetime that means “until the end of the program”. So, this one compiles:

fn get_s() -> &'static str { 
    let q = "I LOVE Jennifer";
    q
}

Now the compiler is happy because the lifetime of the string slice is well defined in the function signature.

So why did it work when the argument was present? The key is “lifetime elision”. In some very common scenarios, where it’s pretty clear what the lifetime should be, the compiler infers them for you. In this case, there is one input reference and one output reference. In such a case, the compiler assumes that these two must have the same lifetime. For details on lifetime elision rules, see the book chapter 10.

Since a string slice with lifetime 'static surely lives at least as long as any input string slice, the lifetime is compatible.


This may be a little abstract, so here’s an attempt to illustrate. Consider this example:

fn main() {
    let s;
    {
        let msg = String::from("I HATE bananas");
        s = get_s(&msg);
    } // msg goes out of scope
    println!("{}", s);
}

fn get_s(m: &str) -> &str {
    let q = "I LOVE Jennifer";
    println!("{}", m);
    q
}

This doesn’t compile. The function get_s is the same as yours. But I changed the input string msg to have a limited lifetime. According to lifetime elision rules, the compiler assumes the output of get_s lives as long as the input argument. Which means, it assumes s becomes invalid at the same time as msg does, so it cannot be printed later.

Now, if you change the return type in the example to -> &'static str, it compiles.

So even though with this:

fn get_s() -> &str { 
    let q = "I LOVE Jennifer";
    q
}

the string literal "I LOVE Jennifer" lasts the lifetime of the program, the “infer” algorithm of the compiler isn’t quite smart enough to infer at a deep enough level to infer the static-lifetime literal is what’s going to be returned, so it complains.

In the similar snippet that includes an incoming value in the signature:

fn get_s(m: &str) -> &str {
    let q = "I LOVE Jennifer";
    println!("{}", m);
    q
}

the compiler assumes (wrongly, but it works out) that the outgoing &str has the same lifetime as the incoming &str? (I’ll have to go read that chapter 10, which I haven’t yet done.)

I may not completely grasp it, but I believe I’m orbiting around the gist. Thank you for taking the time to explain so clearly.

Yes, everything you say is correct as far as it goes.

the “infer” algorithm of the compiler isn’t quite smart enough

I would think about it differently. It is not the goal of the Rust compiler developers to implement a type inference system that infers as many lifetimes as possible. As a matter of Rust’s design principles, function signatures are explicit. The lifetime elision rules are a pragmatic exception to this. There are only three of these rules and they are unlikely to be expanded. The reasons they exist are:

  • They are broadly useful.
  • They are unlikely to give false positives.

An elision rule like this:

fn() -> &str {}
// gets expanded to
fn() -> &'static str {}

…would be easy to implement. But this rule is not broadly useful, because functions with this signature rarely make sense. If you have a static string slice, just put it in a static variable. No need for a function call. So it likely won’t be implemented.

I do recommend the linked section of the book, it should answer any remaining questions.

Thanks, senekor! That’s a solid recommendation, to rethink how I think about the “infer” algorithm, that it’s an exception rather than the rule, for determining a type. That helps nail down my understanding in a way that makes sense.

You’ve been very helpful! I appreciate it!