Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When can string slice `&str` produced by a function be returned in Rust?

Tags:

rust

I've managed to cook up three scenarios which to me seem on the surface to be the same, but behave differently, and I would appreciate if someone could clarify to me why...

The first scenario fails to complile, as I would actually expect :

fn main() {
    let data = get_data();
    println!("{}", data);
}

fn get_data() -> &'static str {
    let x: String = String::from("hello");
    return x.as_str(); // always fails with ownership issue
}

Fails as expected: returns a reference to data owned by the current function

However the next two confound me given the above expectation:

This succeeds:

fn get_data() -> &'static str { // requires `'static` lifetime
    let x = std::path::Path::new("thing");
    match x.to_str() {
        None => "",
        Some(s) => s,
    }
}

I would have thought s was owned in the function so couldn't be returned, but in this case, adding &'static str as return type (compiler is unhappy otherwise) allows the operation.

More puzzlingly still, the following works (compiler is happy), and does not require the 'static lifetime:

use std::path::Path;

pub fn parent_of(caller_file: &str) -> &str { // `'static` not required ??
    let caller_file_p: &Path = Path::new(caller_file);

    match caller_file_p.parent() {
        None => if caller_file.starts_with("/") { "/" } else { "." },
        Some(p) => match p.to_str() {
            None => {
                eprintln!("Bad UTF-8 sequence specified : '{}'", caller_file);
                std::process::exit(100);
            },
            Some(s) => s,
        }
    }
}

I'm sure there's some methodical explanation as to the nuances of each scenario, but it eludes me. Clarifications most appreciated.

like image 949
tk-noodle Avatar asked Feb 06 '26 13:02

tk-noodle


2 Answers

In the second case, you convert a &'static str (a string literal) to &Path via Path::new(). Path::new() preserves the lifetime of the reference (because Path is not an owned type, but is a borrowed type that is basically some wrapper around the bytes owned by someone else, be it a PathBuf, a String, or the binary itself in case of a string literal). So you get &'static PathBuf. Converting it back to &str via Path::to_str() you get &'static str.

In the third case, you have string literals (&'static strs) and &Path with the lifetime of caller_file. If we annotate the lifetimes:

pub fn parent_of<'caller_file>(caller_file: &'caller_file str) -> &'caller_file str {
    // `'static` not required ??
    let caller_file_p: &'caller_file Path = Path::new(caller_file);

    match caller_file_p.parent() {
        None => {
            if caller_file.starts_with("/") {
                "/" // `&'static str`
            } else {
                "." // `&'static str`
            }
        }
        Some(p) => match p.to_str() {
            None => {
                eprintln!("Bad UTF-8 sequence specified : '{}'", caller_file);
                std::process::exit(100);
            }
            Some(s) => s, // `s` is `&'caller_file str`
        },
    }
}

The return type's lifetime is assumed to be 'caller_file via the lifetime elision rules. The string literals "/" and "." are &'static str, and since 'static is greater than (or equal to) any lifetime, they can be shrinked to &'caller_file str. caller_file_p is &'caller_file Path, and its to_str() is &'caller_file str, so this works as expected.

like image 120
Chayim Friedman Avatar answered Feb 09 '26 07:02

Chayim Friedman


@Chayim Friedman has already provided a correct answer but, seeing that there is an other answer which contains some misconceptions. I will therefore add some details about various usages of the word static in Rust to clarify the existing answer.

Literals

Literals are the builtin constructors of the builtin types. These include (among others) numbers, strings (as in &str), and tuples/arrays of those literals. All the types of the values that are literals have a 'static lifetime (we'll come back on this later on).

static values

All binaries have some pre-allocated memory inside the binary itself, the size of which is written within the binary itself. This memory region is allocated with static items in Rust. The type of the values stored in those memory regions is necessarily 'static, since they live as long as the program runs, just like literals.

However, these values are not necessarily literals: the compiler must be able to evaluate the content of this memory region at compile time. Literals can all be computed at compile-time (as there is nothing to compute), but in general all const expressions can be put there too.

In fact, in general, a literal is not allocated as a static value. Most likely, a literal such as 3 will be compiled to an assembly instruction that produces that literal where it is required to be (if not further optimized). It is not created at load-time, and then copied all other the places. But thanks to static promotion, a literal (and other values too) can be promoted as a static value, that is, it will be allocated within the binary, and that allocated region will be pointed to/copied when necessary. The compiler automatically does that when a static value is required.

'static lifetime bound

Certain types T are bound by a 'static lifetime, which is written in Rust as T: 'static. This does not mean that any value x of type T will live as long as the rest of the program, or even that it is possible to borrow a such a value for 'static (ie. &'static x). In fact, most local variables (ie. that are created when a function is called, and which do not exist anymore after that function is left) have a type that is static. Consider the following:

fn assert_static<T: 'static>(x: T) -> T { x }

fn example() {
    // literal, that may or may not be a static value
    let x = assert_static(3);
    // `x` is bound by a 'static lifetime, but it lives at most for the scope of the function `example`
    let x = assert_static(x);
    // this expression is not computable at compile time, it is allocated on the heap
    // and will not live forever, yet it is bound by a 'static lifetime
    let x = assert_static(String::from("hello"));
    
    // the following fails, because while the type of x is bound by a 'static lifetime, it does not have a 'static lifetime, so x cannot be borrowed for 'static
    // let y: &'static _ = &x;
}

A way to think of values whose type is bound by a 'static lifetime is values that could live forever from now on. They do not necessarily exist at the beginning of the execution of the program, and do not necessarily exist until the end.

The reason that the last borrow is illegal is that while the value stored in x has a 'static lifetime, x itself cannot live forever. There is no such thing as a lifetime for variables (at least, not exposed to the programmer) but the idea is that at any point in time, I could move the value out of x and that particular value could live forever, but x itself cannot live forever so any reference to the value stored in x through x is restricted to live at most as long as x does. This is typically what Box::leak does.

This is also the reason why the first example fails to compile, but not the second.

like image 39
BlackBeans Avatar answered Feb 09 '26 07:02

BlackBeans



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!