Return static ref from function

I'm trying to return a static reference from a function.

When the function returns a reference with compile time data it works as expected:

fn make_web_event() -> &'static EventType {
    &EventType::WebEvent(1, DomEventType::Click)
}

However, if I try to return it when it contains one of the function parameters, the compiler says that I am returning a temporary value.

fn make_web_event(id: usize, dom_event: DomEventType) -> &'static EventType {
    &EventType::WebEvent(id, dom_event)
}

error[E0515]: cannot return reference to temporary value
   --> src/node.rs:186:5
    |
186 |     &EventType::WebEvent(id, dom_event)
    |     ^----------------------------------
    |     ||
    |     |temporary value created here
    |     returns a reference to data owned by the current function

I was thinking that since the value would own the id and DomEventType, I would be allowed to do this... However, it seems like I was mistaken. Why exactly is this?

And to try and better explain my confusion, I don't understand how block A, B, and C are really any different in once it's compiled.

edit: I should have explained this better but block A and C work while block B doesn't. Rust Playground

fn main() {
    // block A
    {
        let ev_1: &'static EventType = &EventType::WebEvent(1, DomEventType::Click);
    }

    // block B
    {
        fn make_web_event(id: usize, dom_event: DomEventType) -> &'static EventType {
            &EventType::WebEvent(id, dom_event)
        }

        let ev_1: &'static EventType = make_web_event(1, DomEventType::Click);
    }

    // block C
    {
        fn make_static_web_event() -> &'static EventType {
            &EventType::WebEvent(1, DomEventType::Click)
        }
        let ev_1: &'static EventType = make_static_web_event();
    }
}
1 Like

You're trying to use Rust temporary borrows as if they were what other languages call "return by reference". That doesn't make sense in Rust. Rust references are a very specific case, which is not as widely applicable as passing-by-reference in GC languages or pointers in C-family languages.

&'static is a very unusual case which means that it's a value borrowed for the entire duration of the program, which happens either when something is hardcoded into the executable, or it's a leaked memory (see Box::leak if you really want that). 99.99% of the time it doesn't make sense to use &'static. If you see the compiler suggesting to add 'static lifetime anywhere — ignore it. It's a bad side-effect of how Rust declares that references are forbidden.

When you create a new object, you have to return a self-contained (owning) value, like EventType. It can't be a temporary borrow &EventType, because borrowed values can exist only as long as their owning counterpart, but in your case it's owned by… nothing (or a local variable at best).

If you want to return EventType by reference, then the correct type for this is Box<EventType>. Box and & have identical representation in memory, but Box can exist on its own, while & is only a "half" of a type that refers to some pre-existing storage and can't exist on its own.

3 Likes

Thank you for the answer, that helps to clear it up. To follow up, does the compiler not evaluate ev_1 in my A, B and C blocks to the same thing? Conceptually they same to me, but I have a feeling they are functionally different. Would inlining the function or using const fn change anything?

Can you expand on what you mean by & and Box have identical memory representations? I thought that Box allocated to the heap, wouldn't &'static EventType be referencing a value on the stack?

And to possibly save myself from the XY problem, I know that I'll have a ton of duplicate event types. I originally was going to make an Event store a static ref to it's EventType. My reasoning was that this would reduce memory usage. Is this line of reasoning correct? Should I be approaching the problem differently?

Ignoring lifetimes, the right-hand side in all cases is &EventType. Lifetimes are evaluated contextually, though. In block A, it can't be static, because you created the value right there and it's going to go out of scope (drop) at the end of block A. In B and C, local reasoning would say they could be static, because that's what the signature of the functions declare. However, if the function can't compile due to borrow checking, that's a different problem. (Rust generally analyses functions independently.)

No, those don't affect the lifetime analysis. Sometimes copying code directly out of a function can change the analysis (but it's almost always better to understand why there was a problem in the first place).

A reference (&) can point anywhere -- static memory, the stack, the heap. &'static points to memory that lasts the rest of the run time -- static memory or (rarely) leaked heap memory, but not the stack. A Box does put the value on the heap. It owns this value (and will deallocate it when the Box drops -- goes out of scope). So the ownership gets carried around with the Box, and the lifetime of the value is the lifetime of the Box. The Box data structure itself is a reference (&) to the value on the heap.

I guess I could buy this argument when starting out. Or when you definitely need to handle run-time generated data structures. But I have had it legitimately come up for things which aren't too terribly esoteric, such as using &'static str for input/output representations that are baked into a library, and passing function pointers to other threads.

&EventType::WebEvent(1, DomEventType::Click)

is equivalent of returning a pointer to data in the executable:

static CONST_HARDCODED = EventType::WebEvent(1, DomEventType::Click);
return &CONST_HARDCODED;

but if it's not a constant expression, then this:

&EventType::WebEvent(id, dom_event)

is equivalent of returning a pointer to the stack:

let tmp_variable = EventType::WebEvent(id, dom_event);
return &tmp_variable;

I thought that Box allocated to the heap, wouldn't &'static EventType be referencing a value on the stack?

Rust doesn't really distinguish between heap and stack. They're not special by being this or that. Rust just cares whether memory pointed to by a reference can be proven to stay valid and unchanged for longer than the reference itself.

You can't have &'static pointing to the stack, because everything put on stack is temporary (can be popped), and 'static lifetime specifically means it's not temporary (must not change until program exits). main is not special in Rust, so even variables in main's stack are considered temporary and gone before program exits.

Box<EventType> doesn't have a lifetime. Lifetimes don't apply to types that don't borrow anything, so such types are unconstrained by lifetimes and can be used in any scope.

Box::leak makes &'static references, because leaked memory will not be freed until program exits. Constants can be borrowed as &'static, because they're compiled into the executable, and executable's memory won't go away until the program exits.

2 Likes

That's reasonable, and works just fine with EventType::WebEvent(1, DomEventType::Click).

But keep in mind that EventType::WebEvent(id, dom_event) is not the same thing. It's not a single object, but a set of objects: for each id you need a separate piece of memory to hold this data. There could be (2^63 * dom event types) of them, so it's not something that Rust can pre-allocate space for in the executable. That's not merely an issue of lifetime syntax or language limitation, but a logical impossibility to have nearly infinite set stored in constant space.

You could have something like:

const PRE_BAKED_EVENTS = [
EventType::WebEvent(1),
EventType::WebEvent(2),
EventType::WebEvent(3),
];

and return &PRE_BAKED_EVENTS[id] as &'static EventType with the right id.

See that is part of what confuses me because block A and C compile correctly, while block B doesn't. So in A and C they allow for the ref to have a static lifetime.

I agree with this and think what you're saying make sense. I guess I look at it as knowing that a user is going to click some area (ex a div in the shape of a box) 100 times while the box exists, is there a better way of storing and reusing the event type for the lifetime of the id/node/event target? Or does it even really matter in the long run?

If the event is just 16 bytes per click, then it doesn't matter. Get it working first, microoptimize later.

If the event was large, you could have some sort of reuse based on HashMap<id, Arc<EventType>>.

There are also various specialized arenas in case you needed to allocate lots of these objects and the default allocator wasn't good enough.

If you wanted to work with on-stack values, you could use make_web_event(out: &mut EventType) to have the function write the data to space on stack instead of returning a (dangling) pointer. But make_web_event() -> EventType is likely to optimize to the same thing anyway, so in your case probably the simplest most naive solution is also the most performant.

Ah, I do find this surprising, but the compiler is inferring that the value on the right must be a static value.
(Modified playground example)

And the same is true within the make_static_web_event function.

Apologies for the incorrect explanation.

It's no problem at all, it was part of what was so confusing for me. I thought I knew what would happen, but then the results were actually different. Also I found the other part of your answer extremely helpful, so thank you for the insight.

1 Like

I was thinking about using bumpalo on a different problem, and it may apply here too since the event handlers would be registered and exist in "phases". I think that this is probably the biggest thing for myself:

Get it working first, microoptimize later.

Regardless though I do appreciate you taking the time to help me understand what's actually going on.

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.