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