What is going on here? Why does this compile?

struct Struct {
    x: i64,
    y: i64,
}

fn make<'a>() -> &'a Struct {
    &Struct { x: 5, y: 0 }
}

What does this de-sugar to? When is the referent dropped? Because the longhand version of this (assign value to local variable first, then return a ref to that variable) does not compile (and understandably so).

1 Like

It’s a constant expression so 'a is inferred to equal 'static. If you initialize one of the members with a non-constant expression, the compiler will give the expected complaint.

7 Likes

That feature is described in RFC 1414.

6 Likes

Note that the only reasonable meaning [1] of

// "I can make a reference to a `Struct` with any lifetime from nothing"
fn make<'a>() -> &'a Struct

is synonymous with

fn make() -> &'static Struct

as actually pulling a lifetime out of nowhere is generally unsound.

In fact, if you wanted to represent this as a function pointer or trait object, you will need the latter version:

// Fine
let fn_ptr: fn() -> &'static Struct = make;

// error[E0581]: return type references lifetime `'a`,
// which is not constrained by the fn input types
let fn_ptr: for<'a> fn() -> &'a Struct = make;

In short, if you ever find yourself wishing for a Box<for<'any> Fn() -> &'any Struct> (that pulls some generic lifetime "out of nowhere"), use Box<Fn() -> &'static Struct> instead.


The rest of this reply goes into the weeds, feel free to ignore it.


So, why is the function item allowed to have the first version, that returns 'a "out of nowhere"? After all, it looks like function pointers and trait objects aren't. Is it really exactly the same as the version that returns &'static, and this is some special sugar?

There actually is another difference, in that the former is something this:

// Function item `make` has a lifetime parameter
struct __make_fn_item<'a> { _pd: PhantomData<&'a ()> }
// Generic implementation
impl<'a> Fn() -> &'a Struct for __make_fn_item<'a> { /* ... */ }

(Which is also why you can turbofish make in the playground -- it has a lifetime parameter.)

While the latter is something like this:

// Non-generic function item and implementation
struct __make_fn_item {}
impl Fn() -> &'static Struct for __make_fn_item { /* ... */ }

(If you try the 'static version, you'll find you cannot turbofish it.)

In particular, neither of them are like

// Non-generic function item
struct __make_fn_item {}
// Generic implementation
impl<'a> Fn() -> &'a Struct for __make_fn_item { /* ... */ }

That would allow you to coerce to a higher-ranked for<'any> fn() -> &'any Struct. This is also arguably the version that most intuitively corresponds to the signature ("I can make a reference to a Struct with any lifetime from nothing").

As it turns out, this used to be the interpretation used by the compiler, but no longer is because it is unsound. (Note that you can't turbofish the older interpretation either.) If you look at the implementation of Fn under this old (unsound) interpretation, 'a is uncontrained in the implementation. But the 'static returning version has no generic lifetimes, and in the current interpretation, the 'a is constrained as a parameter of __make_fn_item<'_> (and not really "out of nowhere").

The only way for function pointers to constrain that lifetime parameter is if it is also part of an input parameter to the function itself, hence the difference.

It would be a breaking change to not allow "lifetimes out of nowhere" in function declarations, and there are other cases where the lifetime needs to be a parameter of the function item [2] anyway, so the non-&'static version is unlikely to go away.


  1. in the "what is it practical for" sense ↩︎

  2. "early-bound" ↩︎

2 Likes

Wow this is all very interesting stuff. I must admit however that most of this went over my head (granted I just started learning Rust 2 weeks ago). Thank you everyone for your time. At the very least, I now understand what is going on here and also what it compiles to in asm (under current stable rustc, the make fn is indeed simply loading, via LEA instruction, the constant struct data from the text/data/static - or whatever it's called - section of the asm/binary). I don't fully understand the __make_fn_item struct yet however, nor have I fully wrapped my head around higher-ranked lifetimes. I do feel like impl type and lifetime params could have a more explicit and intuitive syntax for beginners to more easily grasp. It should be more like how basic struct/enum/fn type params work where it's blatantly clear what's going on and what they stand for (e.g. struct Struct<T> {field: T}).

Anyway, it sounds like fn make<'a>() -> &'a Struct doesn't necessarily always mean fn make() -> &'static Struct? Otherwise if it did, I feel like this should be a compiler warning or error as the 2nd version is far less ambiguous to a layman. Regardless, perhaps there's an argument to be made for making this a clippy lint? I can't myself say whether that is warranted however, as I don't understand the situation well enough.

RFC 1414:

Right now, when dealing with constant values, you have to explicitly define const or static items to create references with 'static lifetime, which can be unnecessarily verbose if those items never get exposed in the actual API:

Now this right here is what got me. I guess the shorthand is cool and all but this sort of magic is what ended up stumping me and leading me to a correct program which I didn't understand. With the exception of maybe inline literals/primitives, I feel like all statics and static lifetimes should be required to be declared as so, no? If not outright required by the compiler, maybe this is another thing that could be made into a clippy lint as well. Having a reference expression that is an initialization of a custom type with multiple fields getting inferred as static just feels perhaps somewhat weird to me. But anyways I understand a bit better now and thank you.

References in Rust can't exist on their own. They always need a corresponding owned value to borrow from. It's impossible to make an object and return it by what Rust calls "reference" (the correct way for returning objects by reference is -> Box<Struct>).

Lifetimes are used to track where is the original object that the references are borrowing from.

In your case the make function doesn't take any arguments, so it doesn't have any inputs to borrow from. The only thing it can borrow from is a one global copy of the Struct literal that gets hardcoded into your program. Those special global hardcoded values are given a special 'static lifetime.

Rust is able to implicitly shorten lifetime/scope of shared references. Since 'static is a special case of being maximally long, it's always possible to shorten it to any other lifetime. This makes your function able to say that for whatever lifetime you ask for (<'a>), you can get something that has this lifetime or better (&'a).

5 Likes

As a general bit of advice, if you're confused why something works, try using String instead of simple Copy types like i64. Because you might be correct in general, and just hitting a special case.

For example, if I replace the i64s in your example with Strings

struct Struct {
    x: String,
    y: String,
}

fn make<'a>() -> &'a Struct {
    &Struct { x: "foo".to_owned(), y: "bar".to_owned() }
}

then it gives the error you were probably expecting to see

error[E0515]: cannot return reference to temporary value
 --> src/lib.rs:7:5
  |
7 |     &Struct { x: "foo".to_owned(), y: "bar".to_owned() }
  |     ^---------------------------------------------------
  |     ||
  |     |temporary value created here
  |     returns a reference to data owned by the current function

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=3303ae8578aacb1346f51d7a3ed3b02e


Or to actually answer the question directly, it desugars to something like this:

fn make<'a>() -> &'a Struct {
    static PROMOTED_VALUE: Struct = Struct { x: 5, y: 0 };
    &PROMOTED_VALUE
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=e1521687c00ec38e6313cf3d6a2fe7d4

4 Likes

There's a push-pull issue with Rust's complexity, where various features are added to make the language "easier" by making some things automatic (in some sense) for common use cases. However, this actually makes the language more complicated by adding more rules, and if you hit a use case that isn't "common" enough, you hit a steeper hill of complexity. I guess this could be considered one such case.

In general I do feel that lifetime elision in function signatures is about the most successful such "simplification" though. (Although it didn't play a part in this case. Still, if you prefer explicitness, you should know the rules so you can desugar in your head.)

They're technically different but in a practical sense, they're the same. [1] It's interesting that you find the former less ambiguous than the latter; I find it to be the opposite [2], and it's also the more general signature in that you can use it with function pointers and dyn Fn. The fact that &'static T is can coerce to &'any_lifetime T is very much baked into my head at this point though; maybe that's why.

(I also find it less intuitive at a higher level, which is basically what all the later stuff in my reply was about, but that's also a lot of nuance you should probably just ignore at this point of learning Rust.)

The compiler can't easily go back on letting things be less explicit; it'd be a breaking change. The trend is also to keep making things less explicit, so if you got a Clippy lint it would definitely be of the opt-in variety. This particular case is pretty tame in my opinion; it couldn't have worked without being 'static, so you weren't implicitly opted in to some sort of accidental API guarantee.


  1. There's nitch uses where switching between the two would require small changes; namely you can't use turbofish on the version with no lifetime parameter. ↩︎

  2. I see the "'a out of nowhere" and wonder, where did that come from? ↩︎

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.