Is there such a thing as `by-value references`?

Consider a scenario where T is an object-safe trait. I want to implement a function fn foo(_: impl T) which consumes an object implementing T. However, since I want to put the foo in another object-safe trait, generics are not allowed. One workaround is to pass a Box<dyn T>, but this involves an extra heap allocation.

Should there be by-value references (which I denote &byval Type), where the holder of the reference is responsible to consume a value of Type, then I could declare the function as fn foo(_: &byval dyn T), which effectively allows dynamic dispatch on values.

Is there any library implementing similar functionality?

This doesn't seem like a reference at all. From what I can tell, this would effectively be the unsized_locals feature with T = dyn Trait.

1 Like

Isn't the word "by-value reference" a bit of an oxymoron?

2 Likes

I'm sure you could define a wrapper around a pointer that "owns" the value it points at in the sense that it is responsible for dropping it and such.

Isn't it just a definition of Box, however?

Well, you could do it in a way that allows the actual value to be stored on the stack.

1 Like

I wrote a lengthy post about it:

It's definitely something lacking in the language (we have references which allow shared access to the referee; we have references which allow unique access to the referee, but we don't have references which allow dropping / consuming the referee).

In an alloc-powered world its role is already mainly filled by Box —just look at how magical that type is—, and in other situations, by &mut Option<T> (whereby the callee performs a .take().unwrap()). Sadly, because of that, any attempt to generalize the pattern beyond Box seems to be dimmed too niche to be worth featuring. I suspect we'll eventually have its main roles (dyn FnOnce without Box) magically featured instead —to my dismay; I'll always prefer explicit semantics to magic— when Rust ends up supporting unsized_rvalues (or just unsized_fn_params or whatever). The usual price of that magic will be the weird rules required to feature it / a lack of flexibility. See that post for more info.

There's nothing missing here. Consuming/dropping a value is not the job of a reference. OP's confusion stems from the fact that unsized values currently require indirection, but this doesn't mean that there should be an owning reference type. It means that unsized values should support manipulation by value, which is a logical extension to the language and improves its consistency.

2 Likes

I don't mind unsized_fn_params's sugar being supported, but only if done atop "owning pointers to stack-allocated/local values", which is the missing piece in the picture. Again, we don't miss it that much when we don't mind heap-allocating, but I'd find it baffling if a system programming language's answer to a problem was "too bad if you don't have an allocator around, sorry". Hence, at the very least, unsized_fn_params/unsized_rvalues (partial) support:

But the only way to achieve this is with indirection under the hood! So now the question becomes: should that indirection be exposed, or is Rust now a high-level language with runtime opaque shenanigans low-level users can't interact with? That hasn't been the choice for Futures, despite the async sugar, nor anything else that I know of.

Since language-provided pointers to stack-allocated/local values are, currently, called references, hence the "owning reference" shorthand I've gone with, but obviously this would not be a borrow of the value itself, only of the value's backing storage. Maybe there was some misunderstanding / terminology ambiguity there, regarding:

since,

  1. whilst I reckon the point of a borrow is "to give back", and thus indeed one cannot borrow a value, drop it, and not give it back (but one can, however, borrow the storage of a value, take the value out of it (or use it in_place, e.g., dropping it), and then "give back" the (now "uninit") backing storage of that value),

  2. I wouldn't dare speculate knowing what the "job of a reference" is, and I don't think you should either.

I can, however, mention the FnOnce, FnMut, Fn trinity of Rust, i.e., the "consumes/can drop", "has unique access", "has shared access" trinity.

So, in the references / "pointer to locals" world, we currently have the unique access, the shared access, but not the "consume/drop" access. So there is a piece missing, there, in the full picture.
Rust may not deem it worth it to add it at this point (so we might have to wait for the language after Rust to do so), but I'd personally feel very disappointed if Rust ended up supporting unsized_fn_params or something akin to it, as an opaque high-level blob of sugar, leaving the finer-grained shenanigans out of our reach (again, see that other mentioned post for examples where being able to talk about the lifetime of the backing storage enables writing handy and powerful abstractions, such as dyn-by-value "middleware", which unsized_fn_params will never be able to support, for instance).

I don't get that. If you mean that "in order to access an unsized value in memory, you have to know its address", then 1. that's true for sized values as well, and 2. by that definition, everything technically requires indirection, so that's not useful.

I meant ABI-wise. unsized_fn_params, using &'lt move T syntax for the "owning pointers to borrowed storage", we'd have:

fn foo(f: dyn FnOnce())
// and
foo(|…| { … })

to be sugar for:

fn foo(g: &move dyn FnOnce())
// and
foo(&move |…| { … })
  • (plus, maybe, the fact that f would refer to *g as a place?)

Note the elided lifetime parameter. The point of that sugar, to work, would be that it would necessarily be elided / unnameable.

Hence why such a sugar wouldn't support things such as:

fn bar() -> dyn FnOnce()

would not be supported to start with, since there would be no lifetime to specify for '???:

fn bar() -> &'??? move dyn FnOnce()
  • But if we were given the ability to name lifetimes, we'd be able to write:

    fn bar<'s> (storage: &'s mut StorageFor<some FnOnce()>)
      -> &'s move dyn FnOnce()
    

    (using some Trait to mean existential impl Trait, as written in the original impl Trait RFC)

So, what I'm personally advocating for, is that, thanks to the unsugared syntax (f: &move dyn FnOnce() and &move |…| … are not that alien compared to f: dyn FnOnce and |…| { … }, in my subjective opinion), we'd then be able to easily follow-up with naming those lifetime parameters, which the sugar wouldn't let us do.


If, however, there was some other way, low-level / implementation-wise, to achieve an ABI supporting fn foo(f: dyn FnOnce()), one which would not involve the &move handles / "references" as I see them, then, obviously, my point won't stand. I'm just not aware of such things.

1 Like