Move semantic in Rust

I keep reading on Rust efficiency.

After I read this article I felt like trying multiple variants:

-- Rust
with move semantic aka, default
passing by reference

-- C++ (only look at the my_hasher() section)
With move semantic
Pass by reference

The exact instructions difference is not what matters here. What matters is the Rust's default move semantic : it does too much.

I think if the language used a real move by default, we would end up in a situation where one could pass objects without having to deal explicitly with lifetimes.

It would widen the audience, really simplify adoption because the learning curve would be much lower for anyone interested in coding, the borrow check would do much less work -> lower compilation times -> better productivity.

Visually, it would simplify the language to its finest (subjective, but the simpler the syntax in the usual case, the better: &mut and 'a would go away most of the time, you would just have to deal with mut at most).

As for return values, you would always return by value, because it has not really moved in the first place.

A strong guarantee from the language for efficient moves would help to avoid multiple registry/stack manipulations, or at least minimise them. I don't care about blanket statements of the like: "the compiler is smart enough blabla..".

Before some people take it personally, please take note: a language is just a tool for me, not a religion. And I really care for Rust because it does many things right. Flame wars kept at bay, we are not 5.

You might find better discussion by posting this on the internals forum.

Can you elaborate some on what you consider to be a “real move,” and particularly how Rust’s implementation falls short of your definition?

1 Like

My bad, may a mod move my post there?

The efficient move would have the same efficiency as references in Rust. But you automatically lock the object so that lifetimes are useless because you know the object is there, it has to be there; the compiler should enforce that (easy for immutable but carefully for mut).
As for returns, you basically return the moved in value and the compiler should understand that it refers to the same value as it received. Optimisations could be made easier to reason about and to apply.

To me, it seems like a small change that could help the community. You don't even need to rethink the whole ecosystem. Just adapt it.

And if you really want a copy, use clone() or the like in the caller site. It doesn't change anything to the logic as far as the borrow checker is concerned.

Example of what I am expressing:
this code

It should be guaranteed to do the least instructions possible.

Multiple parameters, would lead to returning a tuple and be optimized away for sure.

There may be corner cases veterans know; May they bring them in?

It's really not clear what you are asking for.

Moves can't be used instead of borrows, and lifetimes cannot go away if we want the compiler to stand a chance of analyzing them. That's why all three constructs exist. It's not because some evil language designer intentionally wants to make the language harder than needed.

You are probably mistaken as to how ownership and borrows work in this language and what they are for. (That's in fact typical of people who propose such radical changes to the memory model.)

Highly doubted.

Would you be able to give an example of this? I don't have a clear understanding of what you're suggesting.

1 Like

Without references, it's hard to write API with signatures such as e. g.

fn get(self: &mut Vec<T>, index: usize) -> Option<&mut T>

In fact, even in some languages that don't have mutable references (not even mutability at all) people like to build up, as far as possible, more or less "equivalent" API capabilities as what true (mutable) references provide in Rust, as it's just so damn useful to be able to directly index into your data's field's subfield's array entry's field, or the like, obtain a reference for a short while, and do some reading and writing through that.

Lifetimes also are only ever nontrivial in cases that are impossible to express at all without references. Only once you hold on to references, put them into data structures, and/or return them from functions, all of these being operations not easily replaced by moving around values by-value, will lifetime even begin to become something that would need any more than zero thought or knowledge. Simple functions with reference-type arguments, called in straightforward ways like foo(&mut my_variable), will not create any complications.

So I'll argue there's little to be gained even in theory[1], as the abilities you get by "working with references but not understanding lifetimes" are already a lot better than, and in particular also include, anything you could do with the "avoid references alltogether in order not to need to worry about lifetimes" approach.


  1. "theory", as I'm obviously ignoring all practicalities of 'Rust already is what it is and offers stability guarantees' ↩︎

4 Likes

What exactly do you mean by it does too much? Going after what is written in the article you link it is better than what happens in c. In c when you pass an argument by value, than the compiler has to copy the value into the stackframe of the function because the value also has to continue existing outside the function.
In rust when you "pass by value" then a move into the function happens. Now the context of the calling function no longer has access to the value. This has the advantage that the optimizer can decide to not actually copy anything but to leave the value outside the function and just refer to it from within the function.

You can emulate c's behaviour by cloning before you pass the value to the function. But AFAIK you cannot emulate rusts behaviour in c.

Argument a passed as a a.clone() &a
c makes a copy doesn't exist passes a reference
Function signature T p - T* p
rust can leave the value as is makes a copy passes a reference
Function signature p: T p: T p: &T

Of course I play it a bit fast and loose here with pointer vs reference and the words copy and clone.
I think c's pass by value will just do a memcpy whereas rust will do whatever clone() has defined, but most likely a deepcopy unless the value implements Copy.
C++ on the other hand will call the copy constructor which will in most cases do something akin to what rusts clone() would do for the same type.

EDIT
From what I have been reading here

My claim that rust can avoid memcopies when moving is actually wrong. Apparently its just the llvm optimizer that will sometimes elide the copies.

Thanks for your answers,

All in all, it is simply a way to ask the compiler to guarantee that when you move a value (in rust's semantic) and you return it, you should behave like a reference. It purges the lifetime management because it's impossible for a function not to have access to all the parameters since they are literally "moved" in. So they have to exist at the same time.

As for the example, I gave the link with the C++ move semantic which mimics the reference one. I don't care about pass by value, except for registers of course. You can see that the result is about the same.

You may differ, and you will, but Python is a simple language and people don't approach it with fear because it is simple. Add the ecosystem at that and people can start working (meaning they understand the problems to solve obviously). But even a beginner can do something.

It would avoid confusion with String and str (just pass a String and do slice in the callee or pass a slice with a proper naming Slice<u8> tells you everything you need: String and &str, seriously? -> confusional naming), don't have to bother with foreign notation like

fn print_refs<'a, 'b>(x: &'a i32, y: &'b i32) {

which would simply be

fn print(x: i32, y: i32) {
}

and

fn do_something(x:i32, y:i32) -> {

  return (x, y);
}

let x : mut i32;
let y: mut i32;
...
(x,y) = do_something(x,y);

would be the move at play.

Rust has a vibrant ecosystem that is exploding already.

That's why Rust should try hard to hide the complexities to lower the barrier.

This is nothing more than that! it is NOT a radical change.

You are missing stack unwinding being safe.

vs Python, Rust hides the complexity of broken code by not allowing as much to compile.

You are missing stack unwinding being safe.

You mean safe as in "while in safe Rust" or safe in general?
What happens to references in that case?

vs Python, Rust hides the complexity of broken code by not allowing as much to compile.

I am talking about the syntax mostly. That's what attracts beginners. And generate more experts later.

Well people, all in all, it's syntactic sugar. Don't go much further than that.

What do you mean by it should behave like a reference? I still don't understand what you actually want.

1 Like

Well, people, forget about all this.
We are good :+1:

I know you decided to close the thread without resolving your concerns, but I would like to point out for other readers who come to this later.

The implication that String and &str are an unnecessary bifurcation is inaccurate. These are different types with different capabilities and different purposes. There are other examples as well: Rc<str> is also a pointer to a string slice, but it provides yet other capabilities not available from String or &str.

2 Likes

Related discussion for the curious.

2 Likes

Maybe Slice could have been an option.. That's simple, talks to everyone immediately..

Overall, what I said has always been diverted in this post.

I am not saying &str is useless overall, just that with a move, we could have deferred to the callee, or renamed &str to Slice and be done.

I keep trying to say that Rust is lacking syntactic sugar AND naming is poor (yeet?).
This is just that AND I made my point by showing the implications in the godbolts that something else was possible.

But since you keep telling me that I am unclear, I stop here. But there is a profound misunderstanding than will never end.

"yeet" is an unstable placeholder explicitly intended to never be stabilized in that form. Chosen to avoid flame wars around "throw" but arguably a failure for other reasons (like this).

2 Likes

But please hear me out: I am convinced by Rust. But it would be better if it had more sugar to it to be much more expressive <-> simpler on the eyes for some idioms.

I am used to many other languages and this is the first time I felt resistance between what I want to express and the way the language makes it happen.

I can perfectly understand that we may have differing opinions, and that doesn't bother me. But when one cannot make his point, even by showing examples, then one has to understand to let go and keep doing what he does. I just failed to make my point clearly. That's on me.

I feel like this can be very counterproductive. Syntax sugar can hide useless details... until those details are not useless anymore, and it becomes even harder to understand them. This is not to say that it is useless, it's just that you need to be very careful when to introduce it.

By the way Rust already has a bunch of syntax sugar around lifetimes, for example this snippet of code you already mentioned:

Can also be written as just

fn print_refs(x: &i32, y: &i32) {

and it will have the same meaning as yours!

But when you start returning references too, or you take parameters like (&T, &mut &T), then the lifetime annotations become relevant, and it would be counterproductive to hide them.

5 Likes