7 ms·
Move in C++ without a std:move
- hn45e7pbij 1mo agoI'd push back slightly on move — at small scale the opposite has been true for me.
- dalvrosa 1mo agoNot sure what you mean, but std::move is one of the greatest tools in C++
- pdpi 1mo agoThis is one case where Rust benefited from C++’s experience — move by default with opt-in clone/copy is IMO the better setup.
- sprocketz 1mo agoAnd the most important idea: destructive moves. Since C++ doesn't track lifetimes it has to leave the object in a "valid state" after a move and the destructor still runs which has to have a check if it should do something or not.
- pornel 1mo agoBTW, Rust's lifetime annotations for borrowed references are a mostly orthogonal feature. Liveness of objects for move/drop semantics is tracked differently, without any syntax and with implicit runtime drop flags where necessary. C++ could probably add the same deinitialized/moved-from state tracking (with an opt-in for back compat sake) purely to avoid dtor bloat, without having to add safety of borrow checking.
- bluGill 1mo ago> C++ could probably add the same deinitialized/moved-from state (with an opt-in for back compat sake) purely to avoid dtor bloat, without having to add safety of borrow checking. There is a lot of talk in the C++ committee about this. The details are complex in some obscure cases.
- account42 1mo agoAt the very leas you'd also need to fix the caller-destructed ABI mess to get a 100% solution.
- siramikvarze 1mo ago[dead]
- jandrewrogers 1mo agoWhile not the common case, C++ move semantics match the situation in systems code where correctness requires decoupling logical object lifetimes and destructors. The object is logically dead but the destructor may be deferred indefinitely for safety reasons. Most code doesn't have the shared memory setups where deferred destruction is necessary.
- gruntled-worker 1mo agoBut the compiler won't necessarily generate any code for the destructor: https://godbolt.org/z/PMando5x4 https://godbolt.org/z/PMando5x4
- affenape 1mo agoIt did for sure, but the problem with C++ is its heritage, specifically that structures can be self-referential. For instance, the Rust's url::Url type has to use usize offsets for tracking the location of each of its components. Conversely, in C++, someone could have already created a similar Url type that would use std::string for the buffer and char pointers for the component locations. As such, you cannot simply memcpy from one struct into another and forget the former as std::string could have its own in-place storage and that would invalidate all pointers - you'll need to define a move constructor instead.
- tialaramex 1mo agoThis is an easy mistake to make but it's not what happened Programmers already knew (~20 years ago) when the C++ move feature was designed that what people want is the destructive move assignment semantic, the thing Rust has today. Other languages did have that. But C++ 98 already existed and WG21 already did not want to make it difficult to take your crusty 10+ year old C++ codebase, slap a sticker on it and say this is "Modern C++" The C++ "move" proposal is slightly sneaky, it admits that what they're proposing is not the destructive move (again, people know they want this) but it gives the impression that if they really want destructive move they can add it later, without revealing what's really going on underneath. In fact C++ move is roughly what Rust would call core::mem::take, we move something in the usual way (a destructive move) but then we replace it with some value of the same type, in the case of core::mem::take it's Default::default() To enable this, C++ is full of types which look superficially familiar to a Rust programmer but have a weird "empty" state to provide that default value where none would make sense. For example std::unique_ptr<T> looks like it's Box<T> but it's not, it's actually Option<Box<T>>, even newer types often do this but they might be more embarrassed about it. [Edited to clarify timeline]
- account42 1mo ago> For example std::unique_ptr<T> looks like it's Box<T> but it's not, it's actually Option<Box<T>>, even newer types often do this but they might be more embarrassed about it. This would be the case even with destructive moves unless you also add some way for std::optional<std::unique_ptr<T>> to be no larger than a pointer by letting std::optional take advantage of the fact that a non-null std::unique_ptr has a bit representation that leaves room for sentinel values. Also, at the language level, C++ moves are sort-destructive as the moved from object only has to guarantee to be able to run the destructor. E.g. you could still have a std::nonull_ptr where move sets the internal pointer to zero but calling anything except the destructor on such an instance throws / calls std::terminate() / is UB. It's only the stdlib types that make additional guarantees - because in most cases it can be done without additional cost.
- siramikvarze 1mo ago[dead]
- bluGill 1mo agostd::move is a great tool when used correctly. However used incorrectly it makes code worse: more verbose and less performant. Since I have no idea how you are using it I can't comment on your experience. My experience is people (including me!) get it wrong fairly often. Fortunately tools can detect a lot of cases where you get it wrong.
- sprocketz 1mo agoMy little trick is to think of them as "oh, I accidentally made an lvalue from an actual rvalue here because a name was introduced, so I need to cast (i.e move() or forward()) back to an rvalue again", that's why i have them as macros: MOVE_CAST and FORWARD_CAST defined as static_cast (also avoids blowing up compile times). I never think in terms of "moving this object".
- jplusequalt 1mo agoThanks Claude.
- tom_ 1mo agoIf the author is reading: both complicated examples are the same.
- quuxplusone 1mo agoYeah, the second one is supposed to read `Apple&& Cat(Apple&& val) { return val; }` — but the return type's `&&` was omitted by accident.
- dkenyser 1mo agoI swear I was staring at these two examples for longer than I care to admit wondering if I was just blind or dumb or both.
- reactordev 1mo agoIt's modern C++ so, a little of both I think. 20 years of it and I still have people saying "I should work on my fundamentals" so it's totally fine to be a bit of both.
- andreasfertig 1mo agoSorry for that mistake! I fixed the last example. The post should make much more sense now.
- OptionOfT 1mo agoYou probably don't pay for a copy or move there as well. -> You probably don't pay for a copy or move there either. (I'm Flemish and made that mistake before).
- fluoridation 1mo agoUnfortunately, I don't think there's getting away from just understanding value semantics to get the correct and/or performant behavior.
- locknitpicker 1mo ago> Unfortunately, I don't think there's getting away from just understanding value semantics to get the correct and/or performant behavior. This is not specific to C++ though. It just so happens that C++ developers who feel this topic is important are those invested in performance optimization. For them, C++ offers them these types of tools. Meanwhile, those who don't have a pressing need to go through great extents to optimize performance can simply fall back to the compiler generating most special member functions and then pay the performance tax of doing deep copies by default. For these cases, which I'd say corresponds to most C++ floating around, it's far more important to know the rules of when to define our own custom constructors and assignment operators.
- bluGill 1mo ago> pay the performance tax of doing deep copies by default. For most code that performance tax is not worth worrying about. There are almost high performance priorities. It is almost always the case that your code runs "fast enough" long before you start worrying about the few nanoseconds a deep copy of a few bytes costs.
- locknitpicker 1mo ago> For most code that performance tax is not worth worrying about. Indeed, I agree. It's possible to go a long way in terms of performance with a basic understanding of passing by reference, without even having ro bother with move semantics. So these topics end up being dominated by language lawyers and the types that enjoy debating "aktualy" topics, who also contribute to making things sound far harder than what they actually are by pretending that this sort of trivia is very important stuff.
- 1mo ago
- sprocketz 1mo agoWhat is that makes NVRO so much more difficult to implement? Why couldn't they mandate that just like RVO? Do compilers literally just special case a simple return statement of a direct construction or something?
- bluGill 1mo agoThe simple cases are simple. However the complex cases get hard. mytype foo() { mytype one; ... if(something) { mytype two; ... return two; } return one; } Is going to be much harder because you don't know are compile time which is returned and so cannot construct the one you return in the correct place. That is just off the top of my head, I'm not a compiler writer, I'm sure they have figured out the simple versions of the above, but you can start to see the complex versions that they can't.
- leni536 1mo agoThat one is not too hard either. `one` simply can't be NRVO'd as there is a runtime path in scope of it that doesn't return it. `two` can be (unless you hide some other return within ...).
- bluGill 1mo agoYou can structre the code differently to get around that. There is a real desire to use NRVO "one" in cases when it is returned as well, if possible.
- fluoridation 1mo agoN/RVO works by (at the machine language level, of course) rewriting the function signature to return void and take an extra pointer parameter, which is written to before returning. If you're returning a newly-constructed object, the compiler can rewrite that into calling the constructor on the pointer, but if you're returning a named object, the class may have a non-trivial destructor that needs to run after the move, such that it's not possible to rewrite uses of the local object into uses of the pointer. I'm not too confident on that last part, because such an implementation would mess with semantics in case of an exception, so anyone feel free to correct me on that.
- ptspts 1mo agoThe HN title is incorrect. It should contain std::move.
- boguscoder 1mo agoTitle made me think they’d just show that move is just a simple cast, nothing more
- vivzkestrel 1mo ago- stupid question, since we are on the topic of c++, i finished reading through learncpp.com - how do I take this from 0 to the guy that builds a play station 3 emulator? - like seriously what kind of step by step projects or learning experiences in increasing order of difficulty do you recommend to get really really good at c++
- simonask 1mo agoGetting very good at C++ takes more than a decade. There are no shortcuts. You will have to start by getting in the trenches and write some code.
- MisterTea 1mo ago> ... and write some code. This but replace some with more. Write more code! Programming is like any other discipline which requires constant training and exercise. Doesn't matter if its Jujutsu or C++, you must train and become well versed in all the aspects of the craft.
- HDThoreaun 1mo agoYou don’t need to be really good at cpp to write a crappy emulator. All the “really good” stuff is about eeking out performance. If you want to build an emulator start with a qt gui and go from there. By the time you’re done you’ll be good enough
- glouwbug 1mo agoThe fun thing about C++ is that it’s really hard. The even more fun thing is that the domain knowledge behind is even harder - game engines, high performance clusters, emulators, real time simulations, rendering engines, high frequency trading - so more often than not most C++ devs commit to learning a language subset and then focus their energy on the task at hand. A PS3 emulator might be a switch statement (not really, but you get the point)
- locknitpicker 1mo ago
- nayuki 1mo agoKudos to the author for explaining these concepts, but I personally find this to be a poor use of my time and mental capacity. I dabbled in C++ programming before Rust 1.0 was released (year 2015). I have spent some time understanding things like RVO, std::move(), rvalue references, T&&, move constructors, and so on. This was the main tutorial that I read a decade ago: https://web.archive.org/web/20240108142848/http://www.thbecker.net/articles/rvalue_references/section_01.html https://web.archive.org/web/20240108142848/http://www.thbeck... I began programming in Rust in 2017, and it is such a breath of fresh air. It has all the power of C++ but shed all the unnecessary baggage (e.g. confusing features, duplicate features). In relation to this article, I like Rust's clear and simple semantics about moving objects. If x and y are variables of type T which implements the Copy trait, then `x = y;` performs a simple bitwise copy. Otherwise, `x = y;` moves the object `y` to `x` and `y` is no longer allowed to be accessed ("moved away"). Whereas in C++, the article states how copy elision behavior has changed over the years: > You get guaranteed copy elision since C++17 in the following case [implying it wasn't guaranteed before C++17] > Then you have named return value optimization (NRVO): [...] The latter is not subject to guaranteed copy elision. You probably don't pay for a copy or move there as well. > While this code compiles, you will get a copy construction of the return value before C++20. > Once you switch your compiler to C++23 mode, both cases do an implicit move. No std::move required. This is why I prefer to deal with Rust instead of C++. And this is just my commentary on specifically the behavior of moves in C++. When I pile on other issues - like copious undefined behavior everywhere (e.g. signed integer overflow, array out-of-bounds accesses), too many footguns that result in bugs and security vulnerabilities - I developed an extreme distaste for programming in C and C++.
- bogwog 1mo ago> I failed to understand something, but it's everyone else's fault
- classified 1mo agoMy rule is: `std::move()` is for (lvalue) arguments. We've had RVO for log enough now. Besides, my `-std` is always ≥ 20.
- ericpiker 1mo agoImplicit move from a variable of type Apple&& is gross, because that reference could refer to something that is not expected to be moved-from. Granted you'd have to be pretty stupid to end up in that situation deliberately. You could accidentally do it and have a bug when you set your computer to C++23.
- dh2022 1mo agoEvery time I run across yet another one of these “let me explain C++ move or copy semantics to you” I thank my lucky star that I am using C# in my day to day work.