back
211 comments
> Let me put this in simpler terms: std::move is like putting a sign on your object “I’m done with this, you can take its stuff.”

and later:

> Specifically, that ‘sign’ (the rvalue reference type) tells the compiler to select the Move Constructor instead of the Copy Constructor.

This is the best conceptual definition of what `std::move` is. I feel that is how every book should explain these concepts in C++ because its not a trivial language to get into for programmers who have worked with differently opiniated languages like python and java.

If you read Effective Modern C++ right Item 23 on this, it takes quite a bit to figure out what its really for.

In simpler terms

1. You must implement a move constructor or a move assignment operator in order for std::move to do anything

2. The moved object could be left in an unusable state, depending on your implementation, after stealing its internal resources.

I thought "move doesn't move" was a fairly common C++ mantra at this point.
Modern C++ is hard to get into for people who learned C++ in the 90s and then worked in other languages for a decade or two.
I read Effective Modern C++ years ago and was confused exactly like what you describe.
> So the standard library plays it safe: if your move constructor might throw (because you didn’t mark it noexcept), containers just copy everything instead. That “optimization” you thought you were getting? It’s not happening.

This is a bit of a footgun and clang-tidy has a check for it: performance-noexcept-move-constructor. However, I don't think it's enabled by default!

Throwing move is super weird too. I believe that it was a mistake to not treat user move like C++11 destructors and default to noexcept(true) on them. But it is what it is.

On the other hand, writing special member functions at all(move & copy constructor/assignment, destructor) is a smell for types that don't just manage the lifetime of an object(unique_ptr like things). People should not generally be writing them and being open to the mistake of getting noexcept wrong.

The reason performance-noexcept-move-constructor is not enabled by default is likely because blindly applying noexcept is dangerous if the underlying logic isn't actually exception-free. If you let clang-tidy slap noexcept on a move constructor that does end up throwing (perhaps because it calls into a legacy member or allocates memory internally), the runtime behavior changes from caught exception to std::terminate().
Most sensible Compiler flags aren't enabled by default... I keep a list of arguments for gcc to make things better, but even then you'll also wanna use a static analysis tool like clang-tidy
performance-noexcept-move-constructor is great but it also complains about move assignment operators, which are completely different beasts and are practically impossible to make noexcept if your destructors throw.
If I'm not mistaken, all the pitfalls in the article have clang-tidy lints to catch
Nothing about clang-tidy is enabled by default, and getting it to run at all in realistic projects is quite a chore.
About 28 years ago, I figured out that I’m just not smart enough to use C++. There are so many foot guns and so much rampant complexity that I can’t keep it all straight. I crave simplicity and it always felt like C++ craved the opposite.
c++ 03 was a lot easier.

For instance, if you want to avoid unnecessary copy operations when returning a string, just return it in variable that you pass by reference (eg. void doSomething(string& str);) likewise avoid the vector class making unnecessary copies, simply by creating the objects on the heap and use a vector of pointers instead of values. It's a bit more ugly, but it works, and you don't need to read a 24 page blog to understand all the corner cases where it can go wrong. modern c++ is all about syntactic suger.

I understand the individual rationales of C++ things but I lost the faith on the whole thing.
I write c++ for a living and I feel the same way. And many c++ codebases have that OOP AbstractObjectInterfaceFactory stink which makes it even worse
C++ is a universal tool with long history. So yes it makes it very complex for various reasons. However it does not preclude one from being productive. I do not come anywhere close to being expert in C++. Still write software that blows the shit out of competition. I have general understanding how the things work and when I need some particular feature I just look up the efficient way of doing it in whatever language. Not just for C++. I actively use many languages. My goal is to deliver good software and get paid by happy client, not to know every little detail of the tools I use, it is just impossible and serves no useful purpose.
Systems programming in the large is hard, owning the category for decades harder still.

Even languages that have tried to fast-follow and disrupt C++ end up looking a lot like C++. There is an irreducible complexity.

Same. I’ve read all the books. Written all these things at least a few times. It’s just not doable post C++11.
Before move semantics the HeavyObject problem was solved in most cases by specializing std::swap for each container.

The design lesson I draw from this is that pursing a 100% general solution to a real problem is often worse than accepting a crude solution which covers the most important cases.

my take looking at languages beyond C++ is a very different one

you want a well working general solution which works well (most of the time for most of the "generic code" (i.e. good defaults for the default use-case).

and then add escape hatches for micro-optimizations, micro-control etc.

C++ on the other hand was deeply rooted designed with micro optimizations and micro control first.

"Generic solutions" where then tried to be added on top, but not by changing a badly working abstraction/design but by adding more abstraction layers and complexity on top. And with a high requirements for back/forward compatibility, not just with the language but ton of different tooling. That this isn't playing out well is kinda not really surprising IMHO. I mean adding more abstraction layers instead of fixing existing abstraction layers rarely plays out well (1) especially if the things you add are pretty leaky abstractions.

-----

(1): In context of them archiving overall the same goal with just different details and no clear boundaries. Layering very different kind of layers is normal and does make sense in a lot of situations. Just what C++ does is like layering "a generic system programming language" (modern C++) on top of "a generic system programming language" (old C++) without clear boundaries.

That still leaves the problem of when to use std::swap vs ordinary assignment in generic (i.e. templated) code.

Like when std::vector needs to resize its underlying storage (as a result of push_back, for example), it has to decide which approach to use to copy/move items from the old storage to the new storage.

For std::vector<std::string>, std::swap would probably be at least ok if not optimal, but for std::vector<int> it would be overkill and therefore decidedly non-optimal. In the latter case, you want to do memcpy(new, old) and be done, not std::swap(old[i], new[i]) for each int.

I think a lot of the motive for adding move semantics to c++ has to do with giving the compiler enough information to produce results that are both optimal and correct in generic code.

You read things like this and, first, you're reminded of Sideshow Bob [1] and it puts Rust concepts in context, namely:

1. Move semantics are to handle ownership. Ownership is a first-class concept in Rust. This is why;

2. C++ smart pointers (eg std::unique_ptr<>) are likewise to handle ownership and incur a runtime cost where in Rust they are handled by the compiler with no runtime cost. Yes you can "cheat" (eg std::unique_ptr::get) and people do (they have to) but this is a worse (IMHO) version than the much-maligned Rust unsafe blocks;

3. Not only do all features have a complexity cost but that curve is exponential because of the complexity of interactions, in this case move semantics and exceptions. At this point C++'s feature set combined with legacy code support is not just an albatross around its neck, it's an elephant seal; and

4. There's a 278 page book on C++ initialization [2].

My point here is that there are so many footguns here combined with the features of modern processors that writing correct code remains a Herculean (even Sisyphean) task.

But here's the worst part: IME all of this complexity tends to attract a certain kind of engineer who falls in love with their own cleverness who creates code using obscure features that nobody else can understand all the true implications (and likely they don't either).

Rust is complex because what you're doing is complex. Rust isn't a panacea. It solves a certain class of problems well and that class is really important (ie memory safety). We will be dealing with C++ buffer overflow CVEs until the heat death of the Universe. But one thing I appreciate about languages like Go is how simple they are.

I honestly think C++ is unsalvageable given its legacy.

[1]: https://www.youtube.com/watch?v=2WZLJpMOxS4

[2]: https://leanpub.com/cppinitbook

> C++ smart pointers (eg std::unique_ptr<>) are likewise to handle ownership and incur a runtime cost where in Rust they are handled by the compiler with no runtime cost.

What additional runtime cost is incurred by the use of std::unique_ptr? Either compared to Rust or compared to doing manual memory management in c++?

Regarding mistake 1: return std::move(local_var), it is worth clarifying why this is technically a pessimization beyond just breaking NRVO. It comes down to the change in C++17 regarding prvalues.

> Pre-C++17, a prvalue was a temporary object.

> Post-C++17, a prvalue is an initializer. It has no identity and occupies no storage until it is materialized.

In C++17 and later, return std::move(local_variable) as opposed to return local_variable is only breaking NRVO (which avoids even having to move, by essentially replacing local_variable with a reference to the variable the caller is assigning the function result to).

In C++17 if you do return std::move(local_variable) it will do exactly what you asked for and move the local variable to the return value, which with copy elision means directly to the caller's variable.

So, return std::move(local_variable) is only preventing NRVO, it's not preventing a move (even though you shouldn't be asking for a move, because move is not the most efficient way).

Should have be called give(). But naming things correctly is hard, and the C++ committee is known to do a lot of things incorrectly
That has about the same issue: like std::move it doesn't really explain that the receiver decides.
There is no giving (or taking).

I think std::rvalue would be the least confusing name.

The name predates the standardisation. The committee did not come with the whole thing themselves, rather they adopted and expanded already existing library implementations. You could move in C++, with this exact name, long before C++11.

See, for example, this implementation https://stlab.adobe.com/group__move__related.html

std::movable
Maybe std::make_movable would have been a slightly better name, but it's so much simpler to write std::move.
C++ is the high rocky mountain pass between the fertile great plains of C and the weird but ultimately survivable California of Rust.
You should almost never ever be writing your own move constructors. Use compiler generated defaults. It's only for very rare specialist classes that you need to override compiler generated defaults. Many times when you think you need to you often don't.
I found the previous discussion and article very helpful

https://news.ycombinator.com/item?id=45799157 (87 comments)

I always understood move as moving ownership, so it's not a misnomer.

> std::move is like putting a sign on your object “I’m done with this, you can take its stuff.”

Which exactly is moving ownership.

Naming things is hard.
Do I really need care about this? I really hoped that I can just not bother wrapping things in std::move and let the compiler figure it out?

I.e. if I have

``` std::string a = "hi"; std::string b = "world"; return {a, b}; // std::pair ``` I always assumed the compiler figures out that it can move these things?

If not, why not? My ide tells me I should move, surely the compiler has more context to figure that out?

> This code works. It compiles. It runs. But depending on how you’ve implemented your types, it might be performing thousands of expensive copy operations instead of cheap moves without you realizing it.

I've spent the last two decades in the .net platform. But for a decade or so before that I was a C++/Unix dev. I remember old style "C with classes" C++ as being fairly small and elegant, and approximately as easy to reason about as C# - albeit that you had the overhead of tracking object ownership and deallocation.

What the language has become now, boggles my mind. I get hints of elegance/power and innovation when I read about it, but the sheer number of footguns is astonishing. I'm very sure that I'm not clever enough to understand it.

But some very smart people have guided the language's evolution. So, what are the forces that have determined the current state of C++?

Sounds more like a contract thing. Of course std::move should be able to throw exceptions (like when it runs out of memory), but when it throws an exception it should still guarantee that memory is in a consistent state.

So the fault here is with std::vector who didn't write that contract.

The best way to think about it is that std::move is a cast.

https://stackoverflow.com/a/42340735

I don’t think this is particularly insightful, as move semantics and r-values are higher level language semantics, nothing more and nothing less.

Rust’s borrow checker doesn’t actually borrow anything either, it’s operating on a similar level of abstraction.

> [std::move silently copies const values, because] If something is const, you can’t move from it by definition.

Whoever wrote that definition should have a thing or two to learn from Rust. Different language I know, but it proves that it wasn't needed to cause so much confussion and collectively so much time and performance lost.

Also, who writes rules like that and ends the day satisfied with the result? It seems unlikely to feel content with leaving huge footguns and being happy to push the Publish button. I'd rather not ship the feature than doing a half-assed work at it. Comparing attitudes on language development and additions, it makes me appreciate more the way it's done for the Go lang, even though it also has its warts and all.