Return struct containing temporary which is bound to the struct's lifetime (&'a str)

Hello everyone! Hope you're having a great day and that seeing this question posted for probably the gazillionth time again won't ruin it!

If I value program efficiency and want to work with this struct:

struct Options<'a> {
    connection_string: &'a str
}

(which is how I think of making this struct efficient), how do I build this struct and return it from a function?

Because I have this code (link to playground) but the compiler doesn't agree with it:

#[derive(Debug)]
struct Options<'a> {
    connection_string: &'a str
}

fn get_options_from_env<'a>() -> Options<'a> {
    Options {
        connection_string: std::env::var("CONNECTION_STRING")
            .unwrap()
            .as_str()
    }
}

fn main() {
    let options = get_options_from_env();
    println!("options={:?}", options);
}

Compiler output:

   Compiling playground v0.0.1 (/playground)
error[E0515]: cannot return value referencing temporary value
  --> src/main.rs:7:5
   |
7  |  /     Options {
8  |  |         connection_string: std::env::var("CONNECTION_STRING")
   |  |____________________________-
9  | ||             .unwrap()
   | ||_____________________- temporary value created here
10 |  |             .as_str()
11 |  |     }
   |  |_____^ returns a value referencing data owned by the current function

error: aborting due to previous error

For more information about this error, try `rustc --explain E0515`.
error: could not compile `playground`.

To learn more, run the command again with --verbose.

Do I need to Box it (Box<str>)?
Do I really need to switch to String?
Is this error particular to &str? I mean what if connection_string was a different struct?

If you can reference a good explanation about why this is happening, I will be happy to read it.

Any other help is greatly appreciated!

std::env::var(…).unwrap() returns a temporary (of type String), with lifetime bound to the function body. Hence, you can't return its address or anything pointing inside it from the function. Why don't you just return the String by value (i.e. store String in the struct) given that you already have one?

2 Likes

In Rust, if you don't return a value from a function, it gets destroyed (or 'dropped') at the end of the scope (that's a bit of a over-simplification, but it's the general idea).

In this case, the value returned by std::env::var("CONNECTION_STRING") isn't getting stored anywhere, so as soon as get_options_from_env returns, it gets dropped. Once that happens, what data is connection_string pointing to? In other languages, you'd have a null pointer (with all the 'fun' that entails), but in Rust, the compiler just says nope.

In this case, the way to fix it would be to just have Options store the String, instead of a reference - Options is what 'owns' the data, so that's where the String should live. You could then pass out &str references to that data, if you wanted.

In general, I'd recommend not thinking in terms of 'references = efficiency' - you're just going to get tangled up in the borrow checker and give yourself a headache that way. Think in terms of 'where does it make sense for this data to get stored'.

2 Likes

Very true. Especially that Box<str> and &str have identical memory representation. & is about ownership, not pass-by-reference.

2 Likes

I'll definitely keep that note close to my cognitive process next time. :grin:

But I still can't get rid of the idea that you can define the Options struct like that but you can't build it somewhere else and return/move it somewhere else. It basically becomes pinned to the context where it gets created, is what it seems to me.

There's really no way that the String returned from std::env::var(...) can be 'converted' to a string slice that will live for as long as Options lives and be owned by Options AAAND be movable to the calling context?

...Perhaps not 'converted' but used to create the connection_string in Options? It can be discarded after that! I guess what I mean is to be able to move the data stored on the heap by String to the local stack and have the String then freed.

For example: creating an instance of the Options struct like this (playground link):

#[derive(Debug)]
struct Options<'a> {
    connection_string: &'a str
}

fn main() {
    let options = Options {
        connection_string: "blah"
    };
    
    println!("options={:?}", options);
}

works!

For me, &str seems be a particular type that I still haven't wrapped my head around... but hopefully with the community's help, I can get past this mental block of mine regarding the use of String vs &str (or even str), and perhaps learn enough about Rust's memory model to get by at the moment, and perhaps not get bored of the language :stuck_out_tongue: (Nah, I don't think so!).

That could be done technically (for example if Rust had variable-sized arrays), and even today it's possible to copy the contents from the String to a longer-living &mut str (or maybe through a &mut [u8]) passed to the function as a sort of "out argument", but it's really not clear why you would want to do all this.

If it's for efficiency, then it's moot because if you already have an owned String allocated, there's no way you can get rid of the allocation overhead once the allocation has been made. Copying the string to the stack wont make the code go faster in retrospective, nor will it consume less memory.

You absolutely can, moving the String by value is how you do that. Don't confuse moving a value with taking a reference to it.

There's nothing special about &str here, by the way. This is the way every value works in Rust: values have a lifetime, and you are not allowed to make dangling references to a value so that a reference would outlive the lifetime of the value it is pointing to.

2 Likes

Maybe this will help:

A String is made up of:

  • A pointer to the memory where the data lives
  • The length of the string
  • The capacity of the memory allocated (i.e. how big can the string be before it needs to reallocate)

A &str is made up of:

  • A pointer to some data within a string (it doesn't have to be the whole string)!
  • The length of the data (again, doesn't have to be all of it)

String is 'a string', whereas &str is 'a view into a string'. This is the same as how Vec<u8> is 'a vector', and &[u8] is 'a view into a vector'.

In fact, String is just a Vec<u8> under the hood, with some extra checks for UTF-8 correctness layered on top.

In this case, the string is static, rather than being created at runtime - therefore, the compiler knows that the reference is always valid. You don't have that guarentee when working with runtime strings.

Or to put it another way - in this case, the &str is a view into the program's binary, rather than a String.

1 Like

That statement is a real mind opener! Thanks for that!

Invaluable insight! Thank you! It's all becoming clearer now!

:bulb: Eureka! Thanks!

Note that your last example would work even if the string literal wasn't 'static.

The real difference is that in that piece of code, you don't return the struct to outside the function, unlike the code in your original question. You only use it inside the same function where you created it, i.e. main(). So, no pointer outlives the contents of the string, and the compiler is happy.

To illustrate this, you could create the struct with a string slice that points to data on the stack, or to data on the heap of which the lifetime is tied to a handle on the stack, e.g. (playground):

#[derive(Debug)]
struct Options<'a> {
    connection_string: &'a str
}

fn main() {
    let local_bytes = [b'h', b'e', b'l', b'l', b'o'];
    let str_1 = std::str::from_utf8(&local_bytes).unwrap();
    let heap_string = String::from("world");
    let str_2 = heap_string.as_str();

    let options_1 = Options {
        connection_string: str_1
    };
    let options_2 = Options {
        connection_string: str_2
    };
    
    println!("options={:?}", options_1);
    println!("options={:?}", options_2);
}

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.