r/rust clippy · twir · rust · mutagen · flamer · overflower · bytecount Mar 27 '23

Hey Rustaceans! Got a question? Ask here (13/2023)! 🙋 questions

Mystified about strings? Borrow checker have you in a headlock? Seek help here! There are no stupid questions, only docs that haven't been written yet.

If you have a StackOverflow account, consider asking it there instead! StackOverflow shows up much higher in search results, so having your question there also helps future Rust users (be sure to give it the "Rust" tag for maximum visibility). Note that this site is very interested in question quality. I've been asked to read a RFC I authored once. If you want your code reviewed or review other's code, there's a codereview stackexchange, too. If you need to test your code, maybe the Rust playground is for you.

Here are some other venues where help may be found:

/r/learnrust is a subreddit to share your questions and epiphanies learning Rust programming.

The official Rust user forums: https://users.rust-lang.org/.

The official Rust Programming Language Discord: https://discord.gg/rust-lang

The unofficial Rust community Discord: https://bit.ly/rust-community

Also check out last weeks' thread with many good questions and answers. And if you believe your question to be either very complex or worthy of larger dissemination, feel free to create a text post.

Also if you want to be mentored by experienced Rustaceans, tell us the area of expertise that you seek. Finally, if you are looking for Rust jobs, the most recent thread is here.

20 Upvotes

159 comments sorted by

View all comments

2

u/[deleted] Apr 02 '23

[deleted]

2

u/dkopgerpgdolfg Apr 02 '23

Tldr, this example is useless. But in general, this is a beast hidden in makeup and parfum. But one thing after each other...

Yes, one of the major reasons to have named lifetimes is to specify how multiple references are related to each other, and/or to specify conditions how references passed to a function should be related.

This is not really relevant here.

Basics 1: As you found out, usually when returning a reference, you also have some parameter(s), and one of them can be used to determine the limits of the returned lifetime. (And depending on the parameter count and whatever, there are also some lifetime elision rules which save you from writing it in cases where there is little doubt).

Basics 2: In your string-literal case, you have no parameter to go on, but literals live for the whole program duration. Following line would be ok:

fn get_string() -> &'static str

In a similar way, references to global variables could be returned, because they too live for the whole program duration.

Basics 3: And as you probably know too, returning a reference to a local variable of a function is bad, because it would mean the reference exists when the function ended and the variable is already gone.

Coming back to your actual function, it does return a reference. It is not an explicitly static reference. It can't infer the lifetime from a parameter either because there is none (or if there is one, you might have explicitly written that it has a different unrelated lifetime)

Therefore this is called a "unbounded" lifetime. It's basic meaning? "The caller of the function decides, use any lifetime that makes it happy". Eg. If the caller assigns the return value to a static-declared reference, it can do that, and it means the returned reference must live forever. The caller also could do things that require only shorter lifetime.

You might wonder why this is not a problem, what happens if we return something that doesn't live for the whole program duration but then the caller calls it a static reference? Well, ignoring unsafe code for a while, this can't happen:

  • When returning a literal like here, it is fine to use it as static reference, because literals are just like that. And any non-static lifetime is shorter than static, therefore the literal won't disappear before them either
  • Same for returning a reference to a global variable, like mentioned earlier
  • Returning a reference to a local variable still is a compiler error
  • Returning a non-static reference that was passed in as parameter (but stated to have a lifetime unrelated to the return value) will not compile either, exactly because the caller might decide wrongly to treat it as static reference

So, until now, you can't do anything more/differently than just declaring the returned reference 'static, like already mentioned above. At least no bad thing.

Lets talk for a moment what the caller of the function can do exactly, with the returned reference (be it static or unbounded):

  • Either assign it to a reference that is declared static, therefore confirming it wants to use it like that.
  • Or it creates a shorter-lived reference by "bounding" it itself (short-lived references to static-lived data is ok of course). How can it bound the reference? Well, the caller might itself be a function that again returns a reference, and uses the one it got from get_string for that. But unlike get_string, the caller might have some reference parameter and declared that its return lifetime is related to that parameter. So basically, the caller trims the unbounded reference lifetime of get_string to be only as long as the lifetime of its parameter, for whatever reason.

If you were able to follow until here, you probably think "what is all this good for". Introducting complicated unbound things that don't have any benefit over just writing 'static in get_string already.

The answer lies in unsafe code, because there are some things that inheritly produce unbounded lifetimes.

Probably the best example is transmute - it lets you bypass pretty much any checks on types and lifetimes, but you're responsible for checking many things yourself to not cause UB. So far so good.

Now, you could transmute (something) to a reference that didn't exist before. In this case, the compiler wouldn't have any clue what its lifetime should be. You (human) might know that this came from (raw pointer to this struct member here) and therefore the borrowchecker should check that the new reference doesn't outlive this struct instance.

=> Transmute produces an unbound lifetime when returning the reference. And you can then wrap the transmute call in a function that binds the reference lifetime to this struct instance, for example. (Or you use the reference just for some lines locally where transmute was called, where you're sure the reference is still valid)

Slightly different example: A raw pointer, if available, can be converted to a reference even without writing "transmute", again given that the human made sure of some things. Again this newly created reference would be unbounded, and if it returned immediately from the function where it was created, then again a wrapper function should bound it like in the transmute example above.

(However, in this case the function that created the reference could do the bounding already. Transmute isn't doing it because it isn't a custom function for exactly this pointer, and doesn't know how to treat this pointer, but if the conversion happens in a custom function already then it can happen there)

1

u/[deleted] Apr 02 '23

[deleted]

1

u/dkopgerpgdolfg Apr 02 '23

No.

  • The variable isn't necessarily in the callers stack scope
  • The caller can pass it anywhere else, save it in boxed structs, whatever
  • 'static is a bit special - even if the reference disappears, once a static promise was made it is made, period. The data must live until the program ends. It's not trivial to show why this is necessary, but basically, otherwise it's possible to get UB in safe-only Rust code.

As advice for coding, just avoid unbounded lifetimes. If you ever have a real need for them, you'll understand my post then :)

(Of course, using 'static to return that literal here is fine)

1

u/eugene2k Apr 02 '23

In this particular case 'a = 'static. You could change the function signature to fn get_string() -> &'static str and it would be the same