Can't return both local variable and reference to that variable

fn main(){
    fn a<'a>()->(Vec<u8>, &'a [u8]){
        let v = vec![0u8; 2];
        (v, &v)
    }
}

v is also returned, but the compiler still says "cannot return value referencing local variable". Furthermore, if I use v.clone() or v.to_vec() when returning, will the compiler optimize out the extra memory allocation?

Returning a reference to a local variable doesn't make sense. When the function returns, the return value is moved, so any references to it would immediately be dangling.

That's entirely unrelated, and it doesn't help with the above problem. It may or may not be optimized away.

In any case, don't try to return a reference to a local. Just return the vector by value.

5 Likes

The returned tuple is a self-referential data type, and such type is not allowed to exist in Rust.

There are some crates with hacky workarounds for this limitation:

1 Like

owning_ref is unfortunately apparently fairly unmaintained and has an API with some significant soundness issues. One of a few well maintained self-referential-types crates that I'm aware of is e. g. ouroboros.

2 Likes

Why?

Rust is designed for all values to be relocatable, i.e. that they can be moved via bitwise copy from one memory location to another without breaking anything. This doesn't hold true for values that contain references to themselves, because the pointer inside the reference would still point to the old location.

The only reason that references work at all in such an environment is that the borrow checker will prevent a move operation if there's a live reference to the value. I believe that it may be technically possible to construct a self-referential object in safe code¹, but if you do, there's very little that you can do with it— In particular, returning a value from a function always requires moving it from the callee's memory space to the caller's, and vice versa for passing it as a function argument.


¹ The &'a mut Something<'a> antipattern looks a lot like a self-referential type to the borrow checker, even though it usually arises from incorrect lifetime annotations instead of actual self-reference.

2 Likes

Short answer: because an alternative was throughly researched in C++ and Rust developers knew where it leads. That's why decision was made: Every type must be ready for it to be blindly memcopied to somewhere else in memory. This means pure on-the-stack-but- still-movable intrusive linked lists are simply not happening in Rust (safely).

Combining moveability with self-references is just leads to insane number of complications, that's why I don't know of any other popular language, except C++, that does that (many managed languages allow something that superficially looks like self-reference, but, in reality, is just a fiction made possible by implicit boxing of almost everything… Rust doesn't want such lies, too).

3 Likes

Because any self-reference would be invalidated when the referred value is moved to another location. This would lead to a dangling reference.

2 Likes

There's also another reason: it would make borrow checking more complicated.

Moves aren't actually a problem for the case of Vec + &[], because the slice points to the heap, not the Vec's metadata stored inline.

However, borrow checking analyzes one field at a time. If it did not consider the field containing Vec to be borrowed, and allowed you to mutate or replace it, then it this could invalidate the slice in another field and cause actually unsafe behavior. If it did consider the Vec borrowed, then this creates a new borrow-checking state where a field can be borrowed and can be moved at the same time. In all other cases this never happens — either something is movable, or is borrowed, but never both at the same time.

The borrow checker is already very complicated and hard to implement, so I presume that adding a new borrowed-but-movable state and distinction between references to values and to the heap would just make everything more painful. Maybe someday Rust 1.200.0 will have it.

1 Like

I think this is a real dealbreaker, it might not even be backward-compatible. Given that references in today's Rust can point anywhere, stripping them of the capability of pointing to either the stack or the heap (or static memory, etc.) would cause massively breakage across the ecosystem.

Not to mention any potential new kinds of reference, which would require updating core APIs pervasively, while also making it impossible to treat memory uniformly (that would also be a pain in the neck).

So the problem is IMO not with the compiler writers' lack of ability to implement a more complicated borrow checker. It would cause headache for the end-user of the surface language as well.

1 Like

That reminds me of

I don’t think we’re done. ...
I think that there are also language extensions, like ... some way to manage self-referential structs, that could fit in this "Fundamental simplifications" category. That’s a bit trickier. The language grows, which is not a simplification, but it can make common patterns so much simpler than it’s a net win.
src: Niko Matsakis: Daring to ask for a more ergonomic, expressive Rust

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.