8 ms·
Move, simply
- butterthebuddha 7y agoMove semantics are a great addition to C++, but off the top of my head, there are two warts: - C++ moves are not destructive (as compared to move semantics in Rust). This means that types must arrange for a valid "moved-from" state. This isn't a huge issue if the type has a natural sentinel value, but not all types have one and I often end up wrapping class members in std::optional to arrange for one. Frankly, this is annoying, because I now have to pay the overhead of std::optional for no good reason (and also use a C++17 compiler, which is not always available). - the syntax for rvalue references and perfect forwarding references is the same, which is the cause for much confusion, especially among beginners. I happily used move semantics for a year before I realized that rvalue references and perfect forwarding references are distinct concepts.
- GeneralMayhem 7y ago>C++ moves are not destructive, as they are in Rust. This means that types must arrange for a "moved-from" state Using an object that has been moved from is only required not to lead to a crash - the object needs to be in a "valid but unspecified state". If there's no sentinel value, you can leave the object in pretty much any state at all, and it's on the caller to not do anything silly before resetting it. There's also https://clang.llvm.org/extra/clang-tidy/checks/bugprone-use-after-move.html https://clang.llvm.org/extra/clang-tidy/checks/bugprone-use-..., which will detect any such silliness.
- roca 7y agoHerb Sutter's article shows why this is actually a big problem in practice. He has an example of a class containing a guaranteed-non-null owning pointer. Leaving an object of this class in a "valid but unspecified state" means when you move out of it, you'll have to replace the internal pointer with a pointer to some new allocation, the very thing move semantics is supposed to help avoid. He suggests that such a class should simply not be moveable. So the fact that moves are not destructive is indeed very limiting.
- AnimalMuppet 7y agoNot quite. Let's suppose that I've got a pointer to a rather large array, and that array can't be null. When I move, the moved-from then has to allocate a new array, to satisfy the can't-be-null constraint. So I have the overhead of the allocation. But I don't have the overhead of copying all the bytes of the array.
- evanpw 7y agoThat workaround is also problematic, because move constructors are supposed to be noexcept, but 'new' can throw std::bad_alloc. So you either have to lie to the compiler and tell it that your move constructor is noexcept (maybe reasonable in this case since a bad allocation will then call std::terminate), or else you have bad consequences like std::vector silently copying your object instead of moving it.
- roca 7y agoSure, but that's almost as bad.
- unlinked_dll 7y ago>So I have the overhead of the allocation. But I don't have the overhead of copying all the bytes of the array. The overhead of allocation is a magnitude greater evil than copying the bytes of the data structure the vast majority of the time. Most data is small and short lived. As well, copying bytes in memory doesn't have side effects and is far more deterministic than allocation. The fact the spec says non trivially-copyable data must be in some "valid but unspecified" state means that move semantics in C++ are borderline useless, except in contrived cases like the one you present.
- AnimalMuppet 7y agoNot quite. It's the "pointer cannot be null" requirement that makes it useless here. If you allow "pointer can be null for a moved-out-of object", then they're much more useful. I'd say that it's the "pointer must not be null, even for a moved-out-of object" that's the "contrived" part.
- deleted 7y ago[deleted]
- Jaxan 7y agoI just started to look into Rust. And while reading the book, I kept thinking: “this is much easier than c++”, exactly for the reasons you mention. In C++ there is quite some things you need to keep in mind while programming (like whether something is moved from), rust takes some of that away.
- ot 7y ago> I often end up wrapping class members in std::optional to arrange for one That's not going to help you unless you explicitly unset the optional in your move constructor. std::optional's move constructor doesn't unset the argument (see #3 in https://en.cppreference.com/w/cpp/utility/optional/optional https://en.cppreference.com/w/cpp/utility/optional/optional). But if you have to implement your own constructors to emplace/unset the optionals, you can just have a sentinel state for your whole object.
- danidiaz 7y agoI also get moves confused with the return-value optimization, though my understanding is that RVO appeared earlier.
- blux 7y agoThe non-destructiveness of move semantics in C++ is very annoying. It causes for example `std::unique_ptr` to not be a zero-cost abstraction in all cases. See https://www.youtube.com/watch?v=rHIkrotSwcc https://www.youtube.com/watch?v=rHIkrotSwcc for a nice expose on this.
- quietbritishjim 7y agoLet me add to that list: - Calling std::move on a const object, or an object without a mine constructor, is not a compilation error or even usually a warning, and it will just silently perform a copy instead (assuming that the result of std::move is used to initialise another object). - Oops, I had to add that disclaimer in brackets, because std::move(foo) is valid code that has no effect if not passed to something else. As much as I understand why, chalk that up as another confusing aspect of mine semantics in C++.
- afranchuk 7y agoWith regard to the rvalue vs perfect forwarding references comment: there's actually no special syntactic difference between the two. The reason forwarding works is because in template <typename T> void do_thing(T&&) T will (with an rvalue) match the actual type, whereas with an lvalue it will match a reference to the type (i.e., you're moving a reference). This is what allows something like std::forward to be in the standard library and implemented in the language itself (without accessing any compiler builtins). That's not to say what's going on there is obvious nor really easy to reason about. It's more a hack that happens to work, like much of the templating ecosystem.
- mannykannot 7y agoI get the impression that the question-and-answer section is circling around the issue of the semantic status of a moved-from variable without quite dispatching it. Is a variable, when it is moved from and thus (if implemented properly) in a valid though unspecified state, not semantically in the same sort of state as a constructed but uninitialized integer, where any bit pattern is valid, but if it happens to be zero and then used as a divisor, will result in a fault? Is it not the case that one of the main reasons for the language giving us the ability to write an explicit no-argument constuctor is to address this sort of problem on construction: it allows us to put the object in an application- or library-defined state, thus establishing a convention that helps with correct use? And is it not a common idiom, when writing a move constructor for a class that has an explicit no-argument constructor, to leave the moved-from object in the same state as if it were newly constructed without arguments? (e.g. std::mutex, where that state is unlocked.) (Update: another common idiom is to swap source and destination.) These are the some of the conventions that make an explicit move robust.
- mark-r 7y agoC++ is designed to be low overhead. Requiring a moved-from object to take on a newly-constructed state will add overhead that won't be necessary most of the time. If you want to add your own convention you're free to do so, knowing what the downside will be.
- mannykannot 7y agoIndeed, though it should at least go through destruction without undesirable side-effects. In the article, however, Herb Sutter appears to be arguing for something stronger: Q: Does “but unspecified” mean the only safe operation on a moved-from object is to call its destructor? A: No. ... Q: What about objects that aren’t safe to be used normally after being moved from? A: They are buggy...
- mark-r 7y agoSurely he doesn't consider unique_ptr to be buggy, so there must be some subtlety that isn't captured by that statement. Perhaps it's in the definition of "normally" - you can't dereference a moved-from unique_ptr, but you can certainly reassign it to a new pointer and go on to use it from there.
- pavon 7y agoHuh, I'm sure he is correct on the intent of the spec, but it is certainly not how our team has been interpreting the allowable state of objects after a move. And while I understand where he is coming from in always wanting the object to meet its invariants until it is destructed, I think this approach will make code harder to reason about rather than easier. I'm a huge proponent that classes should be fully configured on construction and should stay that way until the end of their life-cycle. I really dislike classes that are only partially configured on construction, and then you have to call other methods to complete their initialization. Then every method in the class has to have guard code to check whether the object has been fully initialized. And if you really want to code defensively users of the class also have to handle the case where an instance of that class hasn't been fully initialized, either by checking before calling a method, or by catching exceptions . It complicates things for both the user and the implementator of the class, not to mention adding computational overhead on both sides of the API. If you require objects to be usable after they are moved from then, every object with a move constructor will either need to do extra work during the move to keep itself in a valid (but different) state, or it will need to support being in a partially configured state. In some cases it will be inexpensive to create a valid state (say change an object to point to a preallocated singleton), but in others it would completely defeat the benefit of doing a move to begin with, so you are back to supporting partially configured objects. In the end this seems like a lot of work to keep moved objects valid, when in practice the vast majority of moved objects are implicitly moved temporaries, that are impossible to used after they are moved. And for the cases where an object was explicitly moved, the misconception that you shouldn't use an object after moving it is already well ingrained enough that it rarely happens by accident. I can see putting in sentinels and asserts to sanity check that objects aren't being used after move (and typically do), but I'd still prefer to treat use-after-move as the bug, than complicate the normal uses of the object to support an unnecessary corner-case. Edit: Reworded a few sentences for clarity.
- jstimpfle 7y ago> I really dislike classes that are only partially configured on construction, and then you have to call other methods to complete their initialization. What is "partially configured" after all? IMHO this dichotomy is just a headache brought to you by yours truly, OOP (or rather, syntactically enforced OOP). Personally I don't want to spend the rest of my life pondering such philosophical questions. Or dealing with all the consequences, like constructor exceptions, forced dynamic allocation because static doesn't work without initialization, etc, pp.
- gpu_explorer 7y agoI found it's much easier to understand the concepts behind move operations if you write an object that implemented move semantics by itself. class Object { int* data = new int[32]; void move_into( Object& object ) { object.data = data; data = nullptr; } ~Object() { delete[] data; } } Object a; Object b; a.move_into( b ); I think much of the confusion arises from wondering where is the allocation for int* data located? It's somewhere on the heap. What if the data member were int data[32] instead? What would the class look like? What would 'a' look like afterwards?
- earenndil 7y agoDon't you have to destroy the target object, before you move into it?
- gpu_explorer 7y agoIn this example no because of pointer. Where is the allocation for int* data located? It's not inside the object. You can think easily of the state of 'a' after calling move_into. But maybe with int data[32] as a data member this is harder to reason about. This is a way to illustrate move semantics and confusion behind what happens to the 'moved from' object.
- AnimalMuppet 7y agoYou made the moved-to pointer point to the moved-from allocated heap data. Now nothing points to the heap data that was initially allocated by the moved-to object. So if you don't delete[] the moved-to data pointer, then you leaked 32 bytes of heap.
- gpu_explorer 7y agoWhat happens when the program exits?
- 7y ago
- favorited 7y agoAs someone who only does a little C++, the advice of "that’s pretty much the only time you should write std::move" seems overly simplistic when I read things like this[0] from over the weekend. The gist seems to be that newer standards simplify move semantics, so GCC introduced a warning when you write `return std::move(result);` because the manual move is redundant (and could actually slow down your code). However, if you need your code to run on older compilers, you can get hard-errors by, for example, older versions of GCC not binding the move constructor on a return. Also, because not all compilers have implemented the newer move semantics, you could get copies rather than moves even on newer compilers. So while I'm sure "only call std::move in this one circumstance" will be great advice one day, in the real world at the moment the situation seems decidedly more complex. [0]http://lists.llvm.org/pipermail/cfe-dev/2020-February/064662.html http://lists.llvm.org/pipermail/cfe-dev/2020-February/064662... (mid-thread)
- rsp1984 7y agoC++ “move” semantics are simple, but they are still widely misunderstood. No, they aren't simple and the fact that are still widely misunderstood is basically proof of that. Maybe, just maybe, it has something to do with stuffing rvalue references, perfect forwarding and the whole universal-references-template-clusterfuck into one and the same syntax. [1] The default compiler-generated move can leave behind a null sp member Which is precisely why std::move should be used with caution, which, in turn, is precisely why many codebases are still avoiding it. This problem in particular could have been avoided by not auto-generating move constructors. I really want to like this feature of C++, but I feel its design is just so horribly bad and confusing that I'd feel like a total d..k if I started to force it on a team of developers (of various levels of experience) if there's no crystal clear need for it (and let's be honest, C++ was already pretty successful in getting shit done before there was std::move and rvalue references). [1] http://thbecker.net/articles/rvalue_references/section_01.html http://thbecker.net/articles/rvalue_references/section_01.ht...
- pingyong 7y agoI mean requiring an explicitly defaulted move constructor or something to that effect I wouldn't mind, but personally I also think it's beyond obvious that if you move from a smart pointer that it's going to hold a nullptr after that. I wouldn't consider that hard to understand (or surprising) at all compared to something like even relatively simple thread synchronization, something that comes up for pretty much any native developer eventually these days.
- DoofusOfDeath 7y agoLet me start by saying I have tremendous respect for the people who oversee C++'s evolution. It seems like a very difficult challenge, and I believe their intentions are pure. But I agree completely with your point about "move" semantics. Some language rules that seem clear to Sutter et al are insanely complicated to normal C++ developers. Most of us could probably stay on top of C++'s rules if we spent many hours per week on that task. But few of us have the time or interest to do that.
- dragontamer 7y ago
- codr7 7y agoAnd this is about the point where I finally gave up on C++, I just wish I could get the time I spent learning all the rules and all their exceptions back. It has now morphed into a language where today's optimal code looks horribly inefficient by yesterday's standards. Which adds another layer of uncertainty to what was already a pretty serious mess of a language. Calling this simple is about as silly as it gets. Thanks, but no thanks, I have problems to solve.
- lioeters 7y ago@codr7 - By chance I was just browsing your projects yesterday! g-fu ¹, a Lisp dialect in Go, is a marvel. Other languages you've been creating (gfoo, cfoo, lila) are tastefully done too. So I'm in total agreement with you about the monstrosity that is modern C++. Nobody would have designed such a language from scratch. It seems to be a common fate of popular and long-lived languages (or projects, companies even), that it grows into a complicated monster that would horrify its original creator. ¹ https://github.com/codr7/g-fu https://github.com/codr7/g-fu
- codr7 7y agoI'm glad you enjoy them, writing and using these languages floats my boat but it's always nice to get confirmation that I'm not alone in here :) The biggest issue with g-fu is that it currently expands macros on evaluation. What can I say, it was the first time I implemented quasi-quoting which twisted my brain in exotic ways. That's also why it's so close to Lisp, because it's the only decent macro system I have experience from. g-foo is definitely a cleaner design, but more Forth than Lisp which may not be everyone's cup. Agreed, and Bjarne said so himself; that there's a cleaner, more consistent language hidden deep inside C++. C compatibility has been a blessing and a curse. I think Stepanov is a better designer though; if it wasn't for the STL, I would have given up a long time ago.
- richard78459 7y agoBut the world is built on C++ and there are no alternatives for it. C is too low level and Rust has garbage collector which may not be fit for the domain C++ is suitable for. No alternatives in the domains where C++ shines.
- dragontamer 7y ago> (Other not-yet-standard proposals to go further in this direction include ones with names like “relocatable” and “destructive move,” but those aren’t standard yet so it’s premature to talk about them.) Herb Sutter seems to be burying the lede here. My read on Herb Sutter's post is that the current typical methodology (as represented by the IndirectInt example), is buggy as per the current specification. What we all want is "destructive move". A lot of us believe that C++ move is "destructive move", but it is not. C++ move is this slightly different, simpler move, that isn't in fact the destructive move that we all want. --------- As such, Herb Sutter seems to be pushing for a "True Destructive Move" to be included into the C++ specification. Since std::move is CLOSE to the behavior of true-destructive move, we might as well use it if we're writing code today. But the C++ standard should be fixed to include the concept of a real destructive move. ----- At least, that's my read on things. Anyone else have thoughts? In essence, "C++ Move is simple, perhaps too simple and it doesn't really get the job done". As the specification is currently written, a "moved-from" C++ object is NOT in any special state, like it is in some other popular languages. Programmers seem to expect a special state however (see IndirectInt).
- roca 7y agoIt's going to be amazing when they add a true "destructive move" and everyone has to learn about the different kinds of moves and when to use which one. > a "moved-from" C++ object is NOT in any special state, like it is in some other popular languages. In practice it is; it's in a special state of "satisfies class invariants but otherwise undefined". From the developer's point of view this is very little different from the destructive-move "you can't touch this anymore" state.
- dragontamer 7y ago> It's going to be amazing when they add a true "destructive move" and everyone has to learn about the different kinds of moves and when to use which one. No different than when they added move the first time in C++11 and everybody had to learn the difference between copy and move. No different than Rust programmers learning the difference between Rust's move and C++'s move. Stuff changes over time. In this case, I think it is warranted. The C++11 move is useful in many cases, but not useful enough for how many programmers use unique_ptr<> stuff. Adding that little bit of extra state for the compiler to track might be helpful. > In practice it is; That's the mismatch between C++ Programmers and C++ Language. In effect, C++ Programmers want destructive moves and are currently coding their move statements AS IF they were destructive moves. Herb Sutter is careful to choose the auto_ptr example. Because he's basically saying that unique_ptr is making the same mistake auto_ptr made years ago... the move semantics are subtly different and need to be changed and/or updated.
- 1024core 7y ago> C++ “move” semantics are simple, but they are still widely misunderstood. To use a quote from one of my favorite movies: you keep using that word. it doesn't mean what you think it means! Articles such as this one remind of how C++ is such a design-by-committee language. Watching it evolve is like watching a 100 chefs in a kitchen working on a single dish, where everyone wants the dish to taste the way they like it. This comment is probably not directly relevant to the article, but I had to get it off my chest.
- flukus 7y agoI don't think design-by-committee is the problem, it's more the makeup of the committee. They're all mostly academics, compiler experts, c++ experts and trying to chase some sort of purity. The committee needs someone that represents the mere mortals.
- p2t2p 7y agoThere is a̵n̵ ̵a̵p̵p̵ Rust for that.
- AareyBaba 7y agoThis 20min Chrome Dev video goes over the same topic https://youtu.be/UNJrgsQXvCA https://youtu.be/UNJrgsQXvCA
- typon 7y agoIt's hilarious to me that the guy working on the Chrome performance optimization team and presumably a world-class C++ expert still has trouble with some questions about how the language semantics in certain cases. How do they expect normal people to use this language?
- _wldu 7y ago"About the same time we were starting to work on Go, I read, or tried to read, the C++0x proposed standard and that was convincing to me." -- Ken Thompson Source -- https://www.youtube.com/watch?v=sln-gJaURzk https://www.youtube.com/watch?v=sln-gJaURzk
- millstone 7y agoGo has it bad - the bread-and-butter append() may or may not alias the original array. Even C++ balks at non-deterministic aliasing.
- eej71 7y agoNicolai Josuttis is writing a (not yet completed) book to it. I plan to read it. Maybe then it can be "simple". http://www.cppmove.com/ http://www.cppmove.com/
- gumby 7y agoWhy is an explicit std::move required to pass a named object a as an argument to a && ? Shouldn’t the function’s signature tell the compiler that move is required?
- pavlov 7y agoI think the reason for std::move is that the && function typically has a plain & counterpart (i.e. plain copy). Without std::move, the lvalue (named object) ends up calling that override.
- int_19h 7y ago&& doesn't mean "move here". It means, roughly speaking, "bind to things that are okay to implicitly move from, because they're going away anyway" (e.g. rvalues, or when you are returning a local). std::move() is there to allow you to explicitly say, "this is okay to move from, even though it can still be accessed after the move".
- titzer 7y agoIt's 2020. We have planetary scale computers that do exaflop computations and AIs that can slaughter humans at almost any strategy game. The entire stock market is essentially driven by AI. Drugs are being developed by AI. Bitcoin is consuming more electricity than Ireland. And yet programmers are still fucking around with move constructors, copy semantics, and host of other horseshit foisted upon us by people who think that saving a copy of a couple words of memory here and there should be absolutely foremost in programming. And then one of those designers has the gall to tell us we are all stupid for not understanding this and blowing our legs off every day. C++ is such a waste of everyone's time. Don't even get me started on how much computational power its build system wastes. sigh
- jcelerier 7y ago> exaflop computations Most certainly written in c++ > and AIs that can slaughter humans at almost any strategy game. Guess in what language is trnsorflow written > Bitcoin Ditto
- titzer 7y ago> Most certainly written in c++ Totally beside the point! Actually, FORTRAN mops the floor with C++ when it comes to bigtime numerical computations on supercomputers. And they're all running kernels written in good ole C. Most of the FAANGs are running absolute shittons of Java, Javascript, PHP, and Python. Also running kernels written in C. > Guess in what language is trnsorflow written Derp, tensorflow has to interact with GPU drivers which are all C++ interfaces. Btw it generates shader code which is definitely not C++, but some weird vendor languages, which don't have any of this move constructor related garbage. > Bitcoin Ditto Actually the vast majority of bitcoin's computational load is done by ASICs. Congratulations, you missed the whole point. My point wasn't what languages those things are written in (your Red Herring, not mine), but the fact that we spend an absolute metric shitton of computation on other things, but still expect slow, dumb programmers to fret over niggling details, usually screwing it up in the process, when this task would be far better served by throwing some god damn computational resources at it--you know the ones we waste on O(N^2) compilation tasks and parsing header files over and over and over again.... sigH
- pornel 7y agoThis is what I like about Rust: there are no copy constructors. There are no "moved-from" object values. Moves are guaranteed to be nothing more than a shallow `memcpy`. Structs aren't even copyable unless the author explicitly says so, so if a move compiles, I know it's safe and efficient.
- richard78459 7y agoBut the language is garbage collected, that's not desirable in the domain c++ operates
- Joker_vD 7y agoRust completely dropped GC support in 0.12 — which was released on 9th October, 2014. That happened more than 5 years ago.
- deleted 7y ago[deleted]
- LessDmesg 7y agoC++: a language where you need 10 friggin lectures just to learn to pass variables around. I'm sincerely sorry for anyone who is forced to use this mountain of flaws of a language out of legacy reasons. We as humanity need a plan to reduce the number of C++ lines being written, then to phase out C++ code altogether. This sick roadshow by the ISO committee (and that numbskull Stroustrup) of adding features without fixing any bugs has got to stop. C++ is just horrible, and using it is a waste of time.
- richard78459 7y agoOn a slightly different note, why is the standard not free. I mean not the C++20 but atleast the C++11 standard should be available freely as a single authoritative source.