[HN Gopher] Move, simply ___________________________________________________________________ Move, simply Author : davidmckenna Score : 74 points Date : 2020-02-17 17:59 UTC (5 hours ago) (HTM) web link (herbsutter.com) (TXT) w3m dump (herbsutter.com) | butterthebuddha wrote: | Move 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. | ot wrote: | > 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). | | 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 wrote: | I also get moves confused with the return-value optimization, | though my understanding is that RVO appeared earlier. | GeneralMayhem wrote: | >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-..., which will detect any such silliness. | roca wrote: | Herb 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 wrote: | Not 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 wrote: | That 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. | [deleted] | Jaxan wrote: | I 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. | blux wrote: | The 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 for a nice expose | on this. | codr7 wrote: | And 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. | pavon wrote: | Huh, 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 wrote: | > 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. | 1024core wrote: | > 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. | favorited wrote: | As 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... (mid-thread) | rsp1984 wrote: | _C++ "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.ht... | DoofusOfDeath wrote: | Let 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. | mannykannot wrote: | I 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 wrote: | C++ 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 wrote: | Indeed, 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 wrote: | Surely 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. | mannykannot wrote: | I think so, hence my opening sentence. I think it is also | fair to say that library-writers generally have less | leeway in this matter than application writers, who may | know that certain scenarios are not a problem. | | BTW, I have updated my original post to mention swapping | source and destination, another common idiom for | satisfying the requirement. | dragontamer wrote: | > (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. What Herb Sutter | is proposing 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. | gpu_explorer wrote: | I 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 wrote: | Don't you have to destroy the target object, before you move | into it? | gpu_explorer wrote: | In 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 wrote: | You 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 wrote: | What happens when the program exits? | AnimalMuppet wrote: | Then it doesn't matter. It matters if you run out of heap | _before_ the program exits, though. | mark-r wrote: | No, you don't have to destroy the target object. You may need | to destroy some of its members depending on the object | implementation. | slavik81 wrote: | I appreciate it's just an example, but it should be mentioned | that this is a really bad class. If you write a custom | destructor, you _must_ also specify the move /copy constructors | and move/assignment operators. Otherwise, the same data pointer | will be deleted twice if the object is ever copied. | stinos wrote: | Why would you alter data (set it to nullptr) in a destructor? | gpu_explorer wrote: | From my copy paste example. I fixed thank you. ___________________________________________________________________ (page generated 2020-02-17 23:00 UTC)