11 ms·
C++ std::move doesn't move anything: A deep dive into Value Categories
- shmerl 10mo agoI 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.
- cocoto 10mo agoPersonally I see std::move more like removing ownership because it’s not explicit from its call where the ownership is transferred.
- tsimionescu 10mo agoEven that is a bit suspect, because ownership may well remain with you even after the call, so it's not really removed. For example, this is perfectly valid C++, and it is guaranteed to have no issue: std::string abc = "abc"; std::move(abc); //doesn't remove ownership or do anything really std::print(abc); //guaranteed to print "abc"
- vlovich123 10mo agoStd move doesn’t move ownership. It simply casts into something that could have its ownership taken. Whether or not that actually happens is impossible to identify statically and the value after ownership is consumed is unspecified - sometimes it’s UB to access the value again, sometimes it’s not.
- shmerl 10mo agoMay be disown would be more descriptive, but the point is that it's intended for transferring of ownership versus copying data.
- masklinn 10mo ago> it's intended for transferring of ownership versus copying data. It's intended for transferring ownership, but what it actually does is mark the value as transferrable, whether or not the value is actually transferred is up to the callee.
- mgaunard 10mo agoThat's quite inaccurate. It needs to remain destructible, and if the type satisfies things like (move-)assignable/copyable, those still need to work as well. For boxed types, it's likely to set them into some null state, in which case dereferencing them might be ill-formed, but it's a state that is valid for those types anyway.
- vlovich123 10mo agoWell it’s unspecified what empty/size return for collections after a move. Not a dereference, not UB but unspecified as I said. UB pops up in hand written code - I’ve seen it and the language doesn’t provide any protection here. Thankfully clippy lints do exist here to help if you integrate that tooling
- tsimionescu 10mo agoIt is absolutely knowable statically if ownership will be taken. It's not necessarily very easy to do so, but the decision is 100% up to the compiler, as part of overload resolution and optimization choices (like the NRVO analysis that the article mentions). Since ownership is an inherently static concept, it doesn't even make sense to think about "runtime ownership".
- adrianN 10mo agoMy function can choose to move or not to move from an object based on io input.
- tsimionescu 10mo agoCan you show an example of what you mean? My claim is that, if I call `foo(std::move(myObj))`, it is statically knowable if `foo` receives a copy of `myObj` or whether it is moved to it. Of course, `foo` can choose to further copy or move the data it receives, but it can't choose later on if it's copied or not. Now, if I give `foo` a pointer to myObj, it could of course choose to copy or move from it later and based on runtime info - but this is not the discussion we are having, and `std::move` is not involved from my side at all.
- masklinn 10mo ago> Can you show an example of what you mean? void foo(std::unique_ptr<int, Deleter>&& p) { std::random_device rdev {}; auto dist = std::uniform_int_distribution<>(0, 1); if (dist(rdev)) { auto pp = std::move(p); } }
- tsimionescu 10mo agoThis is exactly what I meant as irrelevant. If I call `foo(std::move(my_unique_ptr))`, I know for sure, statically, that my_unique_ptr was moved from, as part of the function call process, and I can no longer access it. Whether `foo` chooses to further move from it is irrelevant.
- knorker 10mo agoAfter moving a value, it needs to remain in a "valid but unspecified state". How do you mean accessing a valid object is UB?
- drysine 10mo ago>After moving a value, it needs to remain in a "valid but unspecified state". No, it doesn't. The standard library requires that for its classes, but not the language. "Unless otherwise specified, such moved-from objects shall be placed in a valid but unspecified state."[0] [0] https://timsong-cpp.github.io/cppwp/n4950/lib.types.movedfrom#1 https://timsong-cpp.github.io/cppwp/n4950/lib.types.movedfro...
- knorker 10mo agoOk, fair enough. So you're saying if you use the language to write UB, then you get UB? Seems kinda circular. Ok, you're not the same user who said it can be UB. But what does it then mean to same "sometimes it's UB" if the code is all on the user side? "Sometimes code is UB" goes for all user written code.
- drysine 10mo agoI mean the language doesn't dictate what post-condition your class has for move-ctor or move-assignment. It could be - "don't touch this object after move" (and it's UB if you do) or - "after move the object is in valid but unspecified state" (and you can safely call only a method without precondition) or - "after move the object is in certain state" - or even crazy "make sure the object doesn't get destroyed after move" (it's UB if you call delete after move or the object was created on the stack and moved from). But of course it's a good practice to mimic the standard library's contract, first of all for the sake of uniformity.
- masklinn 10mo ago"Validity" is an extremely low bar in C++, it just means operations with no preconditions are legal, which in the most general case may be limited to destruction (because non-destructive moves means destruction must always be possible).
- tsimionescu 10mo agostd::move itself doesn't move ownership, though. It allows the compiler to transfer ownership to the receiver of the value, but it doesn't force it in any way. This is important, because it means YOU may still be the owner of a value even after you called std::move on it. Not to mention, ownership in C++ is not entirely lost with moves in the traditional sense. For example, your code still has to destruct the object even if you did move it to somewhere else.
- HarHarVeryFunny 10mo agoWell, no, because CAN take isn't the same as WILL take. Changing something to an rvalue means it'll now match a move constructor, but there is no guarantee a move constructor will be used, even if defined, because you've got classes like std::vector that are picky and are explicitly looking for a noexcept move constructor.
- fluoridation 10mo agoIn that sense, std::move() is no different than other passing semantics. Just because you wrote at the call site that you want to pass a copy of your object doesn't mean that the callee will actually make a copy of it.
- HarHarVeryFunny 10mo agoI'm not sure what you are saying. If we have foo(std::string a, std string b), and then call it like this: std::string x; std::string y; foo(std::move(x), y); Then x will be moved into a, and y will be copied into b. The callee has no say in this - it's just the compiler implementing the semantics of the language.
- fluoridation 10mo agoWho says there's only one resolution candidate? A different overload could be defined elsewhere that the compiler prefers for that particular combination of arguments, that doesn't cause a copy. std::move() works the same way. The semantics of the operation is defined not by what's at the call site, but by the context.
- HarHarVeryFunny 10mo agoSure overload resolution happens first, but once the compiler has found the correct match then the way arguments are passed depends only on the function signature of that match (callee), and how the caller is passing. An argument passed to a value parameter will be passed by copying, unless it's an rvalue (e.g. forced with std:move) where a move constructor has been defined for that type, in which case it will be moved. The callee has no say in this.
- usefulcat 10mo agostd::allow_move probably would have been a more accurate name for std::move.
- stingraycharles 10mo agoI 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.
- masklinn 10mo ago> Rust’s borrow checker doesn’t actually borrow anything either Why would it? It's called the borrow checker, not the borrower. So it checks that your borrows are valid. std::move looks and feels like a function, but it doesn't do what it says, it makes objects movable but does never moves them (that's up to whatever is using the value afterwards). If you want something similar in Rust, Pin is a much better candidate.
- vouwfietsman 10mo agoSure, but from the perspective of the code that has the move() its good to assume the value is moved at that call, which I guess was the intention of picking the name.
- dathinab 10mo agoyes std::move tells the devs and the compiler that you _intend_ the value to be moved sadly that isn't reflected well in it's implementation as it will "silently" degrade even if it isn't a "move" (1) A `std::move` which fails to compile if it can't be a move(1) it would not have this issues. But it has other issues, mainly wrt. library design especially related to templates/generics, which probably(?) need a `std::move` which works like the current one. I think someone else in this comment section already argued that one issue with modern C++ is too much focusing on the complicated/template/const library design by experts case compared to the "day to day" usage by non experts. (1): There is a bit of gray area in what in rust would be Copy types, for simplicity we can ignore them in this hypothetical argument about an alternative std::move design.
- masklinn 10mo ago
- fooker 10mo agoMaybe std::make_movable would have been a slightly better name, but it's so much simpler to write std::move.
- krior 10mo agothanks to the incredible advances in terms of developer tooling over the last 50 years (i.e. tab-autocompletion) there should be no difference in writing those two.
- kaashif 10mo agoThere is a difference, lots of stuff starts with make_, so lots of possible completions.
- magicalhippo 10mo agoSplit the difference with std::moveable(). Also signals it doesn't actually move, while remaining just as fast to type.
- fooker 10mo agostd::movable is a concept now! https://en.cppreference.com/w/cpp/concepts/movable.html https://en.cppreference.com/w/cpp/concepts/movable.html
- pseidemann 10mo agostd::rvalue
- bitexploder 10mo agoBut that misses too much of the semantics. It also implies ownership transfer, even if copied.
- zabzonk 10mo agoNaming things is hard.
- pseidemann 10mo agoI'm convinced naming things is equivalent to choosing the right abstraction, and caching things is creating a correct "view" from given normalized data.
- oezi 10mo agoThe best way to think about it is that std::move is a cast. https://stackoverflow.com/a/42340735 https://stackoverflow.com/a/42340735
- ahoka 10mo agoThe article explains this.
- rurban 10mo agoShould have be called give(). But naming things correctly is hard, and the C++ committee is known to do a lot of things incorrectly
- masklinn 10mo agoThat has about the same issue: like std::move it doesn't really explain that the receiver decides.
- vouwfietsman 10mo agostd::offer
- repelsteeltje 10mo ago∆ That's actually quite accurate. I like it!
- spot5010 10mo agoMaybe a std::take to pair with it?
- HarHarVeryFunny 10mo agostd::abandon() ?!
- pseidemann 10mo agoThere is no giving (or taking). I think std::rvalue would be the least confusing name.
- usrnm 10mo agoThe 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 https://stlab.adobe.com/group__move__related.html
- MORPHOICES 10mo ago[flagged]
- meindnoch 10mo agoComing from other languages with generics, it took a while for me to internalize SFINAE when writing templated code.
- spacechild1 10mo agoLuckily, with C++17's if-constexpr and C++20's concepts, SFINAE has become mostly obsolete for new C++ code (unless you have/want to support older C++ standards).
- porise 10mo agoValue categories actually just are confusing in a language as complicated as C++. I'm not willing to bet that even senior C++ developers are always going to be able to deduce the correct value category. And worse, in typical C++ fashion, there is still little guaranteed as far as when std::move will actually cause a move. The implementation is still given a lot of leeway. I've been surprised before and you basically have no choice but to check the assembly and hope it continues to be compiled that way as minor changes make their way into the code base.
- nickelpro 10mo ago> even senior C++ developers are always going to be able to deduce the correct value category Depends what "senior" means in this context. Someone with 20-years of domain experience in utility billing, who happened to be writing C++ for those 20 years? Probably not. Someone who has been studying and teaching C++ for 20 years? Yes they are able to tell you the value category at a glance. Language experience is not something you develop accidentally, you don't slip into just because you're using the language. Such tacit experience quickly plateaus. If you make the language itself the object of study, you will quickly surpass "mere" practitioners. This is true of most popular programming languages in my experience. I find very, very few Python programmers understand the language at an implementation level, can explain the iterator protocol or what `@coroutine` actually used to do, how `__slots__` works, etc. C++ is not unique in this, although it is old and has had a lot more time to develop strange corners.
- krona 10mo ago> 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!
- juliangmp 10mo agoMost 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
- teraro 10mo agoWould you mind sharing your list?
- juliangmp 10mo agoSure, I put quickly put them into a small markdown file. At my job we have a cmake interface target that handles these (along with some version checks and project specific stuff), but I can't publish that of course. I might put these into a cmake file at some point, not sure. https://codeberg.org/JulianGmp/sane-args-cpp https://codeberg.org/JulianGmp/sane-args-cpp
- Fiveplus 10mo agoThe 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().
- phkahler 10mo ago
- drob518 10mo agoAbout 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.
- groundzeros2015 10mo agoSame. I’ve read all the books. Written all these things at least a few times. It’s just not doable post C++11.
- benreesman 10mo agoSystems 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.
- zbentley 10mo agoI hear this a lot, but I don’t really understand how this manifests in language complexity like the stuff in TFA in practice. Like, I can understand how systems programming requiring programmers to think about questions like “how can I proceed if allocation fails? How does this code work in an embedded context with no heap?” is hard and irreducible. But I can’t understand why a language’s choice to impose complex rules like C++ move constructor hell is an inevitable outcome of irreducible complexity in systems programming. Put another way: C is also a systems programming language that works for many people, and it doesn’t have any of these Byzantine rules (unless you build them yourself). That’s not to say C is better/preferable, but it swims in the same “official Big Gun systems language” pond as C++, which seems to indicate that revalue semantics as complex as C++’s are a choice, not an inevitability.
- cjfd 10mo agoThe difference is that in C one is supposed to do allocations and deallocations oneself. Then move semantics is just pointer assignment with, of course, the catch that one should make sure one does not do a double-free because ownership is implicit. In C++ ownership is indicated by types so one has to write more stuff to indicate the ownership.
- deleted 10mo ago[deleted]
- Fiveplus 10mo agoRegarding 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.
- HarHarVeryFunny 10mo agoIn 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).
- andyjohnson0 10mo ago> 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++?
- zabzonk 10mo ago> So, what are the forces that have determined the current state of C++? A subset of the language aimed at library writers. As a user of those libraries all these weirdo features are likely to be transparent.
- yosefk 10mo agoTFA explains how std::move is tricky to use and this is not a feature reserved for library writers
- zabzonk 10mo agoOf course it is not reserved for library writers - nothing is. But it is not a feature that application writers should worry about overmuch.
- yosefk 10mo agostd::move is definitely for there for optimizing application code and is often used there. another silly thing you often see is people allocating something with a big sizeof on the stack and then std::moving it to the heap, as if it saves the copying
- deleted 10mo ago[deleted]
- amelius 10mo agoSounds 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.
- groundzeros2015 10mo agoBefore 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.
- dathinab 10mo agomy 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.
- groundzeros2015 10mo agoC++ does have reasonable defaults. You never have to worry about move if you are using standard containers or unique_ptr. But eventually those escape hatches come bite you and you need to worry about. Complexity is inherent to the system. Wrapping it in a nice interface doesn’t make it go away. —- The problem I see is move semantics are a real thing in programming languages where types can own resources. Most languages just choose not to handle them well or limit their feature set. For example swift tries to use copy on write to avoid it So eventually feature creep happens and you get borrowing/move.
- cenamus 10mo agoI found the previous discussion and article very helpful https://news.ycombinator.com/item?id=45799157 https://news.ycombinator.com/item?id=45799157 (87 comments)
- j1elo 10mo ago> [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.
- pjmlp 10mo agoThere was no Rust in 2011.
- j1elo 10mo agoThe point is not a comparison with Rust per-se, but the fact that a better implementation of the idea was mathematically and/or technically possible; and the personal opinion that such huge footguns that the language accumulates over the years are maybe signals of having needed more thought to them before they were considered ready. e.g. if something as simple of a inconspicuous std::move in the wrong place can break the whole assumption about move semantics, then make that impossible to do, or at least do not make it the default happy path, before you consider it production ready. What the heck, at the very least ensure it will become a compiler warning? Hence the mention to Go and how they follow exactly this path of extending discussion as long as needed, even if it takes 10 years, until a reasonable solution is found with maybe small gaps, but never huge ones such as those explained in this article (plus tens of others in any other text about the language)
- pjmlp 10mo agoIt took 13 years to get C++11, actually. Go's discussion is interesting, given how much programming language design history, and flaws of existing languages, they ignore to this day.
- rationalfaith 10mo ago[dead]
- QuadmasterXLII 10mo agoC++ is the high rocky mountain pass between the fertile great plains of C and the weird but ultimately survivable California of Rust.
- jmyeet 10mo agoYou 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 https://www.youtube.com/watch?v=2WZLJpMOxS4 [2]: https://leanpub.com/cppinitbook https://leanpub.com/cppinitbook
- marsven_422 10mo ago[dead]
- usefulcat 10mo ago> 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++?
- injidup 10mo agoYou 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.
- ghm2180 10mo ago> 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.
- dsnr 10mo agoIn 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.
- bitbasher 10mo agoI never understood move semantics until I learned Rust. Everything is move by default and the compiler makes sure you never leave things in an unusable state. This was a difficult mental hurdle to get over with Rust, but once you do, move semantics make a lot more sense. edit: When I said everything is move by default, I mean everything that isn't "Copy", such as integers, floats, etc.
- Conscat 10mo agoWhat Rust loses with that decision is the ability to program the "semantics" in move semantics. Rust has no distinction between hypothetical place constructor and value constructor.
- anematode 10mo agoA loss of functionality, but arguably a good thing, e.g. moving will never throw an exception/panic so you don't need an equivalent to is_nothrow_move_constructible
- ohnoesjmr 10mo agoDo 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?
- brooke2k 10mo agoI think there's a consequence difference between the IDE being sure enough that a std::move is warranted to issue a lint, versus the compiler being 100% provably certain that inserting a move won't cause any issues.
- ohnoesjmr 10mo agoSure, but by the sound of the article, the compiler won't do the right thing? Effectively, I'm a c++ novice, should I ever sprinkle move (under the constraints of the article)? Or will the compiler figure it out correctly for me and I can write my code without caring about this.