this would be a special type of reference that can only appear in function arguments, not in structs (at least at first, for ease of implementation)
unlike all other references, but like previously proposed &out references, these can point to an uninitialized variable (let var;).
if the function returns successfully (ie. it does not panic), the passed variable is considered initialized.
inside a function, such an argument must be assigned to at least once.
as far as the borrow checker is concerned, this is exactly equivalent to the function returning a value than is immediately assigned to a variable, so only code paths where the call definitely happened can use the variable.
unlike &out, this would not require linear typing, as &return references are much more limited in what you can do with them, being closer to syntaxtic sugar than first class types.
this seems most useful with C APIs, as:
C functions cannot return tuples
C functions can't use all the same ABI tricks as Rust functions, such as large struct returns being automatically translated to a output pointer to reduce memcpys
many extant C functions already have these exact semantics (taking a pointer to uninitialized memory), and this would allow those functions to be given a safe signature that doesn't use MaybeUninit or raw pointers.
What happens if the variable is initialized? Is it immediately deinitialized? Is the caller prevented from accessing it if the function panics? Or maybe a temporary gets implicitly inserted and then moved from on success, and the optimizer can sometimes eliminate it?
You mention “this would be simpler than &out” but I don’t know which &out proposal you’re comparing to.
Not OP, but I think it makes sense that if the variable is initialized the compiler should not allow you to pass it (a reference to it? A special reference to it? Syntax should probably be function(&return var)?) as a &return argument.
Unless, of course, the variable is defined as mutable.
unfortunately this is ambiguous in expression contexts, so we would either need to wait for an edition and do some special casing, or have some sort of weird disambiguating syntax, like &<return> x.
As far as I understand, the formal argument is interpreted as a place instead of value and the requirement for it to be uninitialized could be interpreted locally via borrow checking—it's just the inverse of the current requirement. In particular with places we can refer to an uninitialized place just fine in a moved-from state already. Using =a as syntax for an output argument, these could already be properly borrow checked:
let a: u32; // Not initialized
f_initializer(=a);
let a = vec![];
drop(a); // a now moved-from.
f(=a);
The case that seems harder is however assigning through a pointer, i.e. an uninitialized allocation for that output. Because there's currently no way to bind any identifier and path to such a place in a moved-from state except for the very special case that a Box, and only the std Box, allocates a place tracked by borrow checking! (You can re-initialize a box, playground). This isn't very unlikely to be required as output into an allocation pointed to by a raw-pointer is not possible this way. At least assignment syntax would incorrectly drop the uninitialized value behind that pointer. Maybe that is on destructuring assignment syntax to figure out as it presents the same limitation.
Alternatively we might also make the place moved-from on calling in the exact same way that an assignment expression does it. You're allowed to assign to conditionally-initialized places. and it runs drop by checking a hidden flag that it's tracking in the local scope. (Compare with some of the reasoning in the destructuring assignment RFC that is stable. In those words we may accept 'assignee expressions' for the explicit return argument.)
This all begs the question if we need to allow introducing a new variable at the call site. Syntactically let new_var = f() makes the return type special, and it's hard to see how it may work with return arguments while still being an expression. Imo, for consistency, destructuring assignment complements the let syntax, and it'd be weird if in this case we only have access to the former. But we must assign one type to the expression f() so where to put the return arguments with access to pattern syntax.. One confusing difference to the destructuring assignment is that if we drop then it must happen before the function call / right-hand evaluation as part of the argument evaluation, whereas the syntax desugaring suggested in 2909 clarifies it happens after evaluating the rhs. Otherwise, we must pass the init-flat through the ABI and extend borrow checking with them, and I don't think that significant complexity addition to the language is worth this feature.
I think the problem is that the places that would want to use this feature probably don't want to initialize it by moving a value in -- if that was fine they could just use a normal return.
They probably want to get the &mut MaybeUninit<_> and initialize that directly, perhaps by passing the pointer to FFI or something.
TBH I've definitely found places before where this would be convenient. I don't think we need a new type system thing like &return, though.
My instinct is that we could just add a magic thing so you call
If such a function is going to interface with C, I feel like clearer to pass a *mut T to C, let C assign to it, then use unsafe { &*ptr } to get a Rust reference back. And if you want a safe interface over this, just wrap it in a function.
fn report_generating_thingy(report: return Report) {
let data = do_with_report(=report);
// do something more with data
data.forbnicate();
}
Now of course we could return (Report, SomeData) instead but then we again have moves, and the ''return value optimization'' didn't provde anything of value. This is almost a cargo cult from C++ which is needed in the specific form it has due to its non-destructive move and fails to address anything but the most basic optimizations (and even in C++ you fail to get the guaranteed move into fields of the final value when you construct something larger via a constructor). It's absurd.
That's not composable. If BigThing is composed of smaller structures, you can't initialize them one-by-one in this way. I also don't see why one could want to call this explicit_return_place construct more than once in a function, bar some branching. Nor would it handle fallible initialization, i.e. -> Result<BigThing>, or any other more complex composite type. And I don't think that functions utilizing that feature could be themselves composed (i.e. we can't enable placement by return for f() calling g(), where all functions are fn() -> BigThing).
At that point, why not just explicitly pass a *mut BigThing into the function, and just call assume_init at the topmost level? It seems to have the same safety, ergonomics and power, without extra language constructs.
That's definitely an option, but the unfortunate part of it is that it moves the unsafe away from the code more logically responsible for upholding the proof burden. Ideally, you want the "I'm going to initialize this place" code to be the one to say "I promise that I did initialize this place," not the (potentially multiple) callers.
Also, in place initialization is often wanted not just for explicit copy elision, but for constructing pinned data that can't be moved for assume_init.
I believe you could solve this in library with ZST brand lifetime[1] tokens serving as the witness to initialization. But this is unwieldy and generally still requires place moves at the top level, which are hard to optimize out (in part due to MaybeUninit being Copy).
FWIW, you can generate a new brand lifetime with a macro instead of a closure, so this wouldn't need to hose async and/or const. ↩︎