Most explanations of C++'s std::move fail because they don't focus on its actual effect: controlling function overloading.
Most developers have no trouble getting the idea of C++'s function overloading for parameter types that are totally different, e.g. it's clear what foo("xyz") will call if you have:
void foo(int x);
void foo(std::string x);
It's also not too hard to get the idea with const and mutable references:
(Technically it's also possible to overload with rvalue and non-const regular references, or even all three, but this is rarely done in practice).
In this pairing, the first option would be chosen for a temporary object (e.g. foo(std::string("xyz")) or just foo("xyz")), while the second would be chosen if passing in a named variable (std::string x; foo(x)). In practice, the reason you bother to do this is so the the first overload can pilfer memory resources from its argument (whereas, presumably, the second will need to do a copy).
The point of std::move() is to choose the first overload. This has the consequence that its argument will probably end up being modified (by foo()) even though std::move() itself does not contain any substantial code.
All of the above applies to constructors, since they are functions and they can also be overloaded. Therefore, the following function is very similar in most practical situations since std::string has overloaded copy and move constructors:
To clarify, you are saying the point of std::move is that it returns an rvalue reference, allowing the called function to pick the overload variant that is allowed to trample and destroy it's argument?
Specifically, what you did not make clear is the return type of std::move.
std::move is just a cast operation. A better name might be std::cast_as_rvalue to force the overload that allows it to forward to move constructors/etc that intentionally "destroy" the argument (leave it in a moved-from state).
They don't destroy the argument - this is of course a big problem because the semantic programmers actually wanted (even when C++ 98 didn't have move and papers were proposing this new feature) was what C++ programmers now call "destructive move" ie the move Rust has. This is sometimes now portrayed as some sort of modern idea, but it actually was clearly what everybody wanted 15-20 years ago, it's just that C++ didn't deliver that.
What they go was this awful compromise, it's not destroyed, C++ promises that it will only finally be destroyed when the scope ends, and always then, so instead some "hollowed out" state is created which is some state (usually unspecified but predictable) in which it is safe to destroy it.
Creating the "hollowed out" new state for the moved-from object so that it can later be destroyed is not zero work, it's usually trivial, but given that we're not gaining any benefit by doing this work it's pure waste.
This constitutes one of several unavoidable performance leaks in modern C++. They're not huge, but they're a problem when you still have people who mistake C++ for a performance language rather than a language like COBOL focused intently on compatibility with piles of archaic legacy code.
Thanks for pointing this out. It's an absolute myth that C++ move semantics are due to backwards compatibility. The original paper on move semantics dating back 2002 explicitly mentions destructive move semantics by name:
It does bring up an issue involving how to handle destructive moves in a class hierarchy, and while that's an issue, it's a local issue that would need careful consideration only in a few corner cases as opposed to the move semantics we have today which sprinkle the potential for misuse all over the codebase.
I started a new project recently and chose C++ because I wanted cross platform, and a language that let me write the highest performance code I could imagine. C is so lacking in abstractions, I don't think I can deal with it. But C++ is such a pain, I keep looking at Rust and feeling temptation. I'm doing some number crunching, and geometric algorithms, among other things. Not sure if Rust is as good as C++ there.
I'm the wrong person to ask probably because for me Rust seemed like home almost immediately and that's not most people's reaction.
The brute optimisation for Rust is being done by LLVM, just like if you used Clang to compile C++, so your pure number crunching ought to be fine. If anything you may find it's easier to end up correctly expressing the thing you meant with good performance in Rust. If you rely on a C++ library of geometric algorithms, clearly "I can't find an equivalent in Rust" would be a showstopper and so it's worth stopping past crates.io to try a few searches for whatever keywords are in your head
Also, if you know that learning new stuff fogs up your process, you might not want to try to both learn Rust and work on this novel project simultaneously. Some people thrive pairing learning a language with a new project, others hate that and would rather pick, either do something old in a new language, or do something new in an existing one.
If you decide this isn't the right time but keep feeling a twinge, I encourage you to try it for something else, not everybody is going to like Rust, but it's a rare C++ programmer who spends serious time learning Rust and then decides there was nothing they valued from the experience -- particularly if you have no experience in an ML (F# or Ocaml are modern examples)
Thanks. I've learned a lot of languages and enjoy doing it, especially when much of it is a step up, so not a problem there. I may need to just dive in and try it out on a larger project. It was only after doing that with C++ where I really understood what I liked and what I didn't. A lot of the latter is the tooling/IDEs, which doesn't show up reading about the language. One thing I'm not sure about with Rust is porting a UI class hierarchy from C++. Base class `View`, sub classes `Button`, `VStack`, `TextField`, etc. I see how to replace virtual functions with a trait and impls for the various types. But for stuff (fields or methods) shared in the base class, this looks like one area where Rust is uglier than C++.
You can trample and destroy a regular lvalue reference as well. The point of casting to an rvalue reference (and invoking the rvalue reference constructor) is to copy an pointer to the underlying data of one container to a new container and then delete the pointer on the original container (set it to null, not destroy the data). This has the effect of transferring ownership of the underlying data from one container to the other. You can do this with an lvalue reference as well, but the semantics are different.
This is useful for copying the data of a temporary string to another string without actually copying each byte of the data. Since the underlying characters live in the heap, there's no point in copying each byte to a new area in the heap. Instead, use move semantics to transfer ownership of the pointer to a new string container
> This language feature is best understood as an attempt to mitigate the complexity of this other language feature
That doesn't really make any sense. Move semantics aren't meant to mitigate complexity of function overloading. It's more like, it uses function overloading as part of its implementation.
I do strongly prefer Rust's strategy of move being the first class citizen (and being destructive), with copy (/clone) layered over the top. But of course C++ got where it is for historical reasons. And I've never used Rust properly so I don't really know if the grass is greener.
> Rust isn't as far back on that road is it pretends and is likely catching up.
The grass is definitely greener. In Rust you never have any doubts about whether you're moving or copying. Although...
Rust's 'move semantics' are defined as copying the value, including any internal references. That's also what happens in C++, of course, and normally 'the value' is a small struct with potential heap references embedded. But it's possible to embed a large array in a struct, in which case moving it is expensive-
Except usually not, as the memory copy only exists in principle and the optimiser will usually remove it. But that isn't visible at the language level.
It's also nice that the moved-from value cannot be used afterwards. No need to wonder if it's in a sensible state or not -- the type system won't let you access it. (Which is why the optimiser is fairly reliable at reusing the data.)
> It's also nice that the moved-from value cannot be used afterwards.
There are a few cases in low-level systems code where being able to defer destruction of the moved-from value is useful. A common example is when there is no way to enforce ownership semantics on the resource at compile-time e.g. direct I/O or similar types of shared address space. Being able to safely move the value immediately does not imply that you can safely destroy the moved-from value immediately as long references not visible from your address space exist.
The non-trivial moves in C++ are convenient for wiring in the necessary mechanics, automagically ensuring the moved-from value lives long enough for the exogenous references to discover it has been moved. You can do this manually without non-trivial moves but the syntactic sugar makes some things that are inherently memory-dangerous much safer.
> Except usually not, as the memory copy only exists in principle and the optimiser will usually remove it. But that isn't visible at the language level.
that's the biggest difference form C++: a copy or move can, in principle, have observable side effects, so, aside from specifically santioned copy/move elision scenarios, the compiler cannot elide a copy or move constructor unless it can prove there are no visible side effects (usually by inlining the copy/move constructor and optimizing it away).
On the other hand this allows more flexible copy/move behaviour beyond just handling memory.
unneeded complexity is bad. But making simplicity out of a complex problem takes more time and talent than most people have (if it's possible at all for that problem space to begin with).
C++ and Rust are designed around dealing with complex problems. Specifically in this case where memory management is of utmost importance. Understand the problem you're trying to solve before dismissing a tool as "too complex". If you don't have that problem, wonderful. More time to focus on the problems you actually need to solve instead of complaining about other problems.
C++ is definitely complex, probably too complex, but "This language feature is best understood as an attempt to mitigate the complexity of this other language feature" is, aside from being wrong, needlessly inflammatory.
Prima facie: what does std::move look like it's trying to band-aid fix? copy operations?
They expanded the Rule of 3 to the Rule of 5 for a reason. The operations work together. Move semantics simply work as an optimization when you know the data you're taking in won't be needed anymore and you can more or less "snatch" the data (to put it overly broadly). If you can't do that, you take a slow path and just copy properties one by one.
Garbage collection? Well, besides these languages not having that, managed languages are making the exact same decisions under the hood when optimizing. if it knows a reference is being assigned but then goes out of scope, it can do similar move semantics to save on a copy.
Sigh. That statement was literally a very straightforward paraphrase[1] of the upthread point, which I even quoted. That one seems "inflammatory" and the other doesn't speaks more to the reader's perspective than the text's.
But by all means, let's not debate programming language complexity and call people vacuous trolls instead.
[1] "explanations fail because they don't focus on" -> "best understood as". I mean, come on. I'm not reading much bile here. You added that.
Most developers have no trouble getting the idea of C++'s function overloading for parameter types that are totally different, e.g. it's clear what foo("xyz") will call if you have:
It's also not too hard to get the idea with const and mutable references: Rvalue references allow another possibility: (Technically it's also possible to overload with rvalue and non-const regular references, or even all three, but this is rarely done in practice).In this pairing, the first option would be chosen for a temporary object (e.g. foo(std::string("xyz")) or just foo("xyz")), while the second would be chosen if passing in a named variable (std::string x; foo(x)). In practice, the reason you bother to do this is so the the first overload can pilfer memory resources from its argument (whereas, presumably, the second will need to do a copy).
The point of std::move() is to choose the first overload. This has the consequence that its argument will probably end up being modified (by foo()) even though std::move() itself does not contain any substantial code.
All of the above applies to constructors, since they are functions and they can also be overloaded. Therefore, the following function is very similar in most practical situations since std::string has overloaded copy and move constructors: