Is there movement on &move?

I've run into a number of issues recently that really make me want an &move-like construct (basically, an &mut T that makes the referent behave like it has been moved-from). Specifically, this comes up at the intersection of:

  • dyn, because explosive monomorphization is bad for microcontrollers.
  • no_alloc, so no Box<dyn>.
  • Traits for simple state machine transitions, i.e. what would normally be written as follows:
trait State1 {
  type Assoc: State2;
  fn foo(self) -> Self::Assoc;
}

As a workaround, this winds up getting rewritten into

trait State1 {
  type Assoc: State2;
  fn foo(&mut self) -> &mut Self::Assoc;
}

type DynState = dyn State1<Assoc = dyn State2>;

But, now I'm really sad because I've lost Rust's explicit move-checking and have something that has a closer feel to a C++ move constructor. If I could instead have written

fn foo(&move self) -> &move Self::Assoc;

then I would be less sad while still being object safe: the place I took an &'a move winds up behaving like something I took an &'b mut of, where 'b is 'a extended until that place goes out of scope.


The most recent discussion I could find on this is `DerefMove` vs `&move`, and walking the tree of links doesn't make me feel confident about whether there was any consensus about how to specify this. It also seems to be mostly motivated by building DerefMove, whereas the primitive I think I want is something resembling this:

impl<T: ?Sized> RefCell<T> {
    // Future calls to `borrow` and `borrow_mut` always fail.
    // Unfortunately this is incompatible with `get_mut`. Ugh.
    fn borrow_move(&self) -> &move T;
}

At any rate, my question: is this moving forward? Is this a thing other people want/lang-team people care about?

I'm not sure about the motivation – why does a state machine need to move from, and if you want to move something, why can't you take it by value? If the reason you can't take it by value is that it is unsized, wouldn't it be more productive to move the unsized locals feature forward instead of adding an entirely new feature?

My understanding is that unsized rvalues can't do what I want, which is to make traits which take self and return associated types object-safe. That is, given

trait State1 {
  type Assoc: State2;
  fn foo(self) -> Self::Assoc;
}

I want to form the type dyn State1<Assoc=State2>.

AFAIK we don't have a story for returning trait objects by value.

why does a state machine need to move from

"State machine" is probably the wrong word; what I'm building is closer to a linear pipeline that might normally be written like this:

let first = ...;
let second = first.consume();
let third = second.consume();
// And so on...

This enforces a strict order of operations. I don't know of a good name for this idiom. My version is made up of traits.

and if you want to move something, why can't you take it by value?

Because object safety. As you point out, unsized rvalues solves this, but it does not solve returning a trait object by value.


Of course, if we've figured out a way to return trait objects that is portable and doesn't do horrible things to the stack, this whole discussion is kind of moot.

Do I understand correctly that you have one additional requirement/wish, specifically the ability to use non-object-safe traits as trait objects? Because that is orthogonal to operating on values vs. references. (In addition, I don't think it's possible – the object safety rules exist for technical reasons.)

I was also almost certain that trait objects with associated types can't be implemented for the same reason as generics: You can't monomorphize the generic code without knowing the types at compile-time, but trait objects actively prevent you from knowing the types, so their combination is simply a logical contradiction. AFAIK making this not a contradiction would require a deeply radical change like making all Rust programs have RTTI (run-time type information) by default, which obviously isn't going to happen.

I have no idea how &move references would help with anything like this.

If I understand this correctly.. I think I may see another user case. Anonymous enums are hotly debated now. What it boiled down for me to (I'm using @eaglgenes101 's syntax) was a light-weight conversion:

let a: A = ...;
let ab: (A|B) = a;  // we're talking about something akin to Haskell's Either - an enum with 2 members with "names" (A|B)::0 and (A|B)::1; what has just happened could have been spelled let abc = (A|B)::0(a)
let bca : (B|C|A) = ab;  // we're talking about an enum with 3 members with "names" (A|B|C)::0, (A|B|C)::1 and (A|B|C)::2; what has just happened is a conversion - data has been copied from ab to bca and discriminant has been changed from 0 to 2 in the process
assert_eq!(bca, (B|C|A)::2(a));

So here we copied ab to bca changing discriminant in the process. But what if we wanted to change discriminant "in place"? Rust would need to forget the old variable and know to "reuse" same memory location as a new variable of a new type. Could &move be made useful here?

let a: A = ...;
let ab: (A|B) = (A|B)::0(a);
let bca : &move (B|C|A) = (&move ab) as &move (B|C|A); // no memory copied, just an in-place update of discriminant

@mcy did you mean something like this instead?

trait State {
  fn transition(&move self) -> &move dyn State;
}

How can this work if C - and thus the whole enum - is larger than (A|B)?

Indeed, it cannot :frowning:

Wild fantasy: if bca was somehow both &move and read-only...

I think the particular point that was missed here (which maybe I glossed over) is that given

trait Foo {
  type Assoc: Bar + ?Sized;
  fn foo(&mut self) -> &mut Self::Assoc;
}

we can form the trait object type dyn Foo<Assoc=dyn Bar>, even though dyn Foo on its own is not a valid type. When writing dyn Trait, and Trait has associated types, they must be specified up-front (this avoids the RTTI issue you allude to). I get the impression that this is not a well-known feature...

Though the compiler seems to provide a nice error message for it: https://godbolt.org/z/fsbcK3


This is well and good, but unfortunately, this only works when we specify the associated type in positions that are all unsizeable, which means that you can't play the same trick with the following trait:

trait Foo {
  type Assoc: Bar + ?Sized;
  fn foo(self) -> Self::Assoc;  // We need Assoc to be sized!
}

See: https://godbolt.org/z/79dbqY

The reason I bring up &move is because it is one option for fixing this. I could rewrite by trait into something like this;

trait Foo {
  type Assoc: Bar + ?Sized;
  fn foo(&move self) -> &move Self::Assoc;
}

With unsized rvalues, I could straight-up take self, but I don't believe I can return an unsized type, so I'm still sad.

Of course dyn Foo is not a type, because it is generic, it's a type-level function. I'm still not sure how this relates to using it by-value vs. by-ref, if unsized locals and unsized by-value moves are allowed, ie. what (and why) would be missing in that case.

I can't talk for the OP, but the reason I personally like "owning pointers" / &move is precisely that it allows implementing unsized locals.

Here is a proof of concept manually implementing the feature with unsafe, a macro, or by infecting everything with contiuation-passing style:


It looks like there even is an experimental crate out there, which does mention the unsized locals alternative.

I have a problem with that on the level of principle. Unsized locals as a core language element would be a natural extension that could "just" be allowed without requiring any particular explicit feature per se. Unsized locals wouldn't complicate the language either concretely or conceptually. They don't seem like something that should actively be "implemented" – rather, they should be allowed by making the compiler smarter.

On the other hand, adding a completely new pointer type like &move would make the language grow and it would require APIs to account for that as well (most prominently, blanket impls for &T and &mut T would be needed to be augmented with the new type).

This seems like the abstraction would be pointing to the wrong direction, so to speak.

For the pure purpose of owning references, there's already the standard Box, Rc, etc. types and the owning_ref crate.

You need to be able to return unsized rvalues. AIUI, we have so far failed to come up with a good ABI for this kind of thing. Unsized rvalues are a simple conceptual solution, but implementing them to behave completely like traditional rvalues is non-trivial.

I work on microcontrollers that can't malloc (as stated prior, I'm no_alloc). If I could throw a Box at the problem I wouldn't even be bringing this up.

1 Like

Sure, it might well be. I'm still in favor of trying to allow treatment of unsized values (whether named or temporary) just like sized values (and making the necessary effort) instead. Adding &move references is non-trivial too. That's not a good argument against unsized moves.

I still don't see how having &move and unsized rvalues were ever implied to be incompatible. I think both would be a fine addition to the language, and arguing about unsized rvalues fails to answer the question, "has there been movement on &move and related proposals". If the answer is "no, unsized rvalues do everything we want already", then so be it, but I would like someone familiar with the &move proposals to answer that question.

I'm not sure there is "someone familiar with the &move proposals", because AFAIK there never was a "serious" &move proposal, in the sense of one that had clear compelling motivation and was fully fleshed out and had multiple regular community members supporting it. Every time I've seen it suggested in the past it was your typical incomplete or unmotivated proposal that we politely explain the incompleteness of and then it goes nowhere. And as this thread has already demonstrated, every problem space I've ever seen &move references suggested as a solution for turned out on closer inspection to either not be solved be &move references at all or have a far better solution in some unrelated feature.

Perhaps the more interesting question is whether there's been any movement on unsized rvalues. That I can confidently answer with a "no".

2 Likes

What about RFC-2884?

Throwing in a link for convenience:

1 Like

Anonymous enums have been brought up so I just wanted to give my thoughts on them. I don't think they are the best solution, although I guess they theoretically could serve a similar purpose. The proposal I have been workshoping, and others as well include the concept of enum impl Trait which is essentially an opaque enum generated by the compiler where all variants implement some trait. Currently this is only to return multiple local types from the same function.

fn some(x : Option<usize>) -> enum impl Fn() -> usize {
    match x {
        Some(v) => || factorial(x),
        None => || 0,
}

Although I suppose it could theoretically be used as a return type of a trait function where the enum contains all possible return types of all implementations of a trait.

trait Foo {
    fn foo() -> enum impl SomeTrait;
}

In this situation all implementations would need to register their return type with the trait, then the trait would return some compiler defined enum of all possible variants. While probably not an impossible computation I don't know if it would be a feasible one. This means there would need to be some sort of global state across all crates that would need to maintain this enum as implementations are compiled. There is also a lot of guns aiming at your foot with this approach.

impl Foo for Bar {
   fn foo() -> impl SomeTrait {
        [usize; 1_000_000] // it's all fun and games until someone does this
   }
}

This would however means that the return type of foo() is technically a concrete sized type. Although it would be opaque, you wouldn't know what type it actually is. It's essentially a stack allocated trait object.

I suppose a similar approach could be used with unsized types as well

fn foo() -> enum of str {
    if random() {
        *"Hello"
    else {
        *"Hello World"
    }
}

let hello : enum of str = foo();
println!(hello)

I'll be honest though I don't know enough to tell you if this approach is even doable or not.

Oh, neat. This idea is completely nuts but actually the thing I want. Hopefully this will be the solution I need some day. =)