logoalt Hacker News

C++ std::move doesn't move anything: A deep dive into Value Categories

225 pointsby signa11last Friday at 9:01 AM181 commentsview on HN

Comments

ghm2180yesterday at 4:43 PM

> 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.

show 4 replies
kronayesterday at 10:43 AM

> 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!

show 2 replies
MORPHOICESyesterday at 10:27 AM

Value categories and move semantics are great examples of programming concepts that can cause confusion, and it's a great example of how not having a bad documentation can still lead to confusion through bad mental models. ~

Intuitively you think you understand what is going on, and you think you can answer what is going on, and you can even use it due to understanding it on an operational level, but you can't explain it due to your confusion.

As a result, you most likely are going to create a lot of small bugs in your software and a lot of code that you don't really understand. So, I'm curious to know what others think.

What concept did you learn later than you thought you would? What knowledge did you struggle with the most? What finally helped you understand it?

show 2 replies
drob518yesterday at 11:39 AM

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.

show 3 replies
groundzeros2015yesterday at 12:45 PM

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.

Fiveplusyesterday at 11:44 AM

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.

rurbanyesterday at 10:06 AM

Should have be called give(). But naming things correctly is hard, and the C++ committee is known to do a lot of things incorrectly

show 3 replies
fookeryesterday at 9:00 AM

Maybe std::make_movable would have been a slightly better name, but it's so much simpler to write std::move.

show 4 replies
QuadmasterXLIIyesterday at 3:47 PM

C++ is the high rocky mountain pass between the fertile great plains of C and the weird but ultimately survivable California of Rust.

injidupyesterday at 4:30 PM

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.

ohnoesjmryesterday at 5:14 PM

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?

show 1 reply
cenamusyesterday at 12:47 PM

I found the previous discussion and article very helpful

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

andyjohnson0yesterday at 11:51 AM

> 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++?

show 3 replies
zabzonkyesterday at 9:11 AM

Naming things is hard.

show 1 reply
ameliusyesterday at 12:25 PM

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.

jmyeetyesterday at 4:08 PM

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

show 2 replies
oeziyesterday at 9:16 AM

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

https://stackoverflow.com/a/42340735

show 1 reply
shmerlyesterday at 8:36 AM

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.

show 4 replies
rationalfaithyesterday at 2:38 PM

[dead]

stingraycharlesyesterday at 8:47 AM

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.

show 1 reply
j1eloyesterday at 1:03 PM

> [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.

show 2 replies