Hacker News new | past | comments | ask | show | jobs | submit login
Move, simply (herbsutter.com)
117 points by davidmckenna on Feb 17, 2020 | hide | past | favorite | 92 comments

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...

> still widely misunderstood is basically proof of that.

How much better the world would be if software developers started making judgements based on empirical evidence instead of rhetoric and navel gazing...

To paraphrase the old bit:

If an idiot misuses your software in the morning, you have a user who's an idiot. If idiots misuse your software all day, you're the idiot.

> No, they aren't simple and the fact that are still widely misunderstood is basically proof of that.

The article disproves itself. Sure, move is simple, if you stick to that one very narrow use case (that still needs std::move() boilerplate around moves to signal intent).

The problem (that the article then goes to point out) is that there are so many non-simple and buggy ways move can (and does) get used.

So you don't just need to learn the simple use case (with added boilerplate), you also need to learn all the complicated buggy use cases so that you can avoid using them.

For actual "simple" moves, try Rust.

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.

"Simple" is an overloaded English term. Its not that "Move is so simple that you should understand it". It seems to be "Move is so simple it doesn't actually get the job done that most people want".

I 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.

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.

@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

I'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.

But 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.

Rust has as much garbage collector as C++ has share_ptr. Put simply, it doesn’t have.

There is definitely more C than C++ running under the hood on most computers. C is the language where there are no alternatives if you ask me. Go is garbage collected and C++/Rust are both infinitely more complicated.

Ada 2012?

Perhaps no other _popular_ alternatives but still there are alternatives.

> (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).

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.

> 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.

> 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.

> No different than when they added move the first time in C++11 and everybody had to learn the difference between copy and move.

Yes, it's not the first time the cognitive load of C++ has increased.

> No different than Rust programmers learning the difference between Rust's move and C++'s move.

Learning concepts in a new language doesn't increase cognitive load in the same way. In C++ you will have to decide which kind of move to use in any given situation, and deal with code using multiple kinds of moves. Learning Rust doesn't create such issues.

> Adding that little bit of extra state for the compiler to track might be helpful.

It'll be interesting to see how that goes. In Rust the compiler ensures safe code can't access a moved-out-of object, but that depends on Rust's strong aliasing guarantees. In C++ I guess accessing a moved-out-of object will have to be a new kind of undefined behavior, creating new classes of bugs and requiring new sanitizers and approximate static checkers to be implemented.

Happily the undefined behavior plan for C++ seems to be, in addition to putting all the UB definitions in one place, requiring all new proposals justify why something should be UB or UD.

E.g. no more "it requires work to say what happens" UB cop outs - if something can be defined it should be.

In this case I'd say that a value cannot be used after it is destructively moved. There is no reason for that to be UB, when the language can just refuse to compile it.

In general the compiler will not be able to prove that the value is not used again, because of possible aliasing. E.g. if you called a method on the object, then the compiler would need to be able to prove that 'this' hasn't been stashed away somewhere so it can be used again (without otherwise triggering undefined behavior) after the object has been moved out of.

I.e. to have the compiler prove absence of use-after-move requires the same sort of lifetime analysis it would need to prove absence of use-after-free, and that is simply not realistic for C++.

Use after free (through references) is a standard problem in C++ through anything. But you can say post-move a value is dead, and use of existing references would be UB (sigh, but that isn't new).

But to address your point: I'm a muppet and didn't think about conditional move in a loop.

I had been thinking that you require that any conditional block has to end with a moved value int he same state - e.g.

if (a) {move(x)} else {}

The compiler would say both blocks must end with x being dead, so the else block would be required to call x.~X();

But yeah, that fails completely if you have

for (...i...) { if (i) move(x) }

because my clever rule means that x would have to die at the end of the first loop.

But the general problem I have with std::move currently is that there's no guaranteed death - the fact that post std::move a destructor will still run, or the object can just continue to be used in general means that your code has to be made defensive against already being logically dead - even if just the destructor.

Rust takes a conservative approach here --- you can't use x if it might have been moved along some control-flow path leading up to the current point. C++ could do that too.

> Use after free (through references) is a standard problem in C++ through anything. But you can say post-move a value is dead, and use of existing references would be UB (sigh, but that isn't new).

OK, arguably aliased-use-after-move could be treated as an existing form of UB. Sanitizers and static checkers would still need to be updated, however.

Destructive move would be incompatible with c++ as it works today. If you construct an object it’s valid. Since move doesn’t do delete() (how could it?) it leaves behind a block of memory that the program considers an X and so must be valid.

The compiler can just say "this value is dead, you can't use it".

That's what rust does - rust isn't literally freeing/clearing memory when a struct is moved.

Rust can do this for an arbitrary element of a dynamically allocated array?

No. You can't move an element out of an array in Rust.

What Ollie said is true for local variables, and collections generally support some kind of "move element(s) out of the collection" API (e.g. Vec::pop(), T::into_iter()).

So rust can do it in a subset of cases, such that it can make a stronger assertion; C++ can do it in the general case, and is therefore unable to make the stronger assertion.

Neither is correct vs the other, rather they reflect different choice points in the language possibility space.

You can do a C++ style "swap object with a placeholder" move in Rust, easily, with std::mem::replace(). There's no advantage for C++ here.

Rust can only do that in unsafe blocks.

The C++ version would need the existing (unchangeable) move semantics.

So, what do you propose std::move<int> should do?

Initializing the ‘source’ int to zero seems the only reasonable option, but it would hurt performance.

Also, what about std::move<T> in general? If T has a destructor, should that call it on the original? Currently, deciding whether that (or doing its equivalent) is a good idea is up to the implementer of std::move<T>. I don’t see how the compiler could make that decision without sacrificing performance in some cases.

I think that, if you want the compiler to enforce destruction of moved-from values, you also should want the compiler to zero out (or equivalent) all destructed values.

std::move has been part of the language for 9 years. It is no longer reasonable to change std::move.

To satisfy the programmer, there probably should be a std::dmove(x), which would be a compiler-enforced notation that would make use of x illegal after the use of std::dmove.

std::dmove(x) would be implemented as std::move, followed by a destructor, and then the compiler removes variable x from the scope. Pointers and references to that variable will have undefined behavior if they are used (compiler may find it reasonable to recycle x's memory location to another variable)


std::move<int> remains the same. There's no performance benefit, or even code-logic benefit, to zeroing out integers. For example, an integer may be a denominator, dividand, modulus, or multiplier. In these cases, "zeroing out" to "x = 1" makes more sense than "x=0".

Having a compiler enforced "zero" of "x=0" is arbitrary and doesn't help anybody. Its better to let the integers remain the same value they used to be.

But the whole point of std::move is to avoid calling, possibly expensive, destructors.

The standard example is

  template<typename T>
  void swap(T& x, T& y)
    T z = std::move(x);
    x = std::move(y);
    y = std::move(z);
  std::string a[1000];
  std::swap(&a[2], &a[7])
If std::move copy-constructs and destroys, that last line would do three allocations and three frees (recoverable by a significantly advanced compiler, but who has that?). The way std::move is specced, it can do zero allocations and zero frees.

A good point.

So clearly the way std::move works now is great for that situation. But there's a 2nd situation, with Herb Sutter's IndirectInt, which calls for a different behavior.

>So, what do you propose std::move<int> should do?

Just copy Rust. Trivially copyable types aren't invalidated by moves, and "move" just aliases copy.

Is the intention of std::move<unique_ptr> that the "moved-from" pointer no longer has its destructor run?

What if you move a unique_ptr from a std::vector? You don't know which elements of the vector need to have their destructors run.

I think Rust unconditionally doesn't run the destructor of a moved-from Box, but uses drop flags for "maybe-moved-from" local variables, and doesn't allow maybe-moved-from Vec elements.

My point was on handling the moving of trivially copyable types, like int, not those with side effects like unique_ptr. For trivially copyable types, copy and move are (almost) synonymous, and can be made such in Rust with #[derive(Copy)].

But that difference actually gets to your point here. The only difference between copy and move is that a move allows for the new data to overlap the previous. So if you think about what moving from a vector of unique ptr actually means, it must invalidate the vector or remove the element that was moved from the vector. If you want to get the element from the vector without those effects, you have to create a deep copy.

Basically moving without invalidating or mutating such that the moved-from alias can no longer be accessed is necessary.

You’ve referred to two kinds of move there — memmove (overlapping) vs memcpy (non-overlapping memory) and transfers of ownership. In C++ a move appears to always require moving memory with either (depending on aliasing) or via registers. Is that correct?

In Rust, the only thing that has to happen is a transfer of ownership, which does not always mean moving bytes. This greatly simplifies the whole “rvalue reference parameter” thing, because ownership abstracts over wherever you might want to move to. So if your new owner wants it in part of its new stack frame, the compiler is free to just put it there before calling or simply call a destructor on the parent stack frame’s memory in the child. (I don’t know exactly what it likes to do, but it’s all fine.) New owners like Box need to move memory, and that’s cool too, and the compiler is free to reorder the steps to avoid initialising and moving when “placement new” is better. This is also something I believe the compiler is getting better at slowly. The compiler won’t let you safely write moves out of a vector without mutating or taking ownership of the vector, each with moving bytes — otherwise it has access to something it no longer owns.

The easiest example is `let a = A::new(); let b = a;`, which only needs stack space for one A, and for which line 2 is a noop. Its only effect is in when drop is called, if b is in a smaller scope. Does C++ do this too? I would hope so, but it looks like no.

Edit: turns out neither do it.

- https://rust.godbolt.org/z/xjA9v4 - https://godbolt.org/z/7Wh_AZ

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.

> 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.

To me the question isn't a philosophical one, but a practical one. The more states an object has, the more special cases you have to handle, the more likely you are to get one wrong. The more methods an object has that can only be used when the object is in a particular state, the more likely the user of the class it to make a mistake.

Sure, but states are just there, and acting like they weren't can even increase complexity. For example, RAII basically eliminates the "memory is allocated bit uninitialized" state by means of enforcing an automatic transition, at the cost of being no longer able to allocate many objects sratically.

C++ doesn’t believe in “partially constructed”. The real world sometime has this unfortunate event but your code is supposed to deal with it.

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.

>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.

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.

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.

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.

>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.

Not 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.

Sure, but that's almost as bad.

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.

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.

Let 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++.

> 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.

With 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.

I also get moves confused with the return-value optimization, though my understanding is that RVO appeared earlier.

> 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.

I 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.

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.

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.

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...

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.

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.

C++: 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.

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)

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;
        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?

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.

Yes I know this of course but it seemed that leaving things out is faster and people can know it's just a toy.

Don't you have to destroy the target object, before you move into it?

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.

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.

What happens when the program exits?

Then it doesn't matter. It matters if you run out of heap before the program exits, though.

No, you don't have to destroy the target object. You may need to destroy some of its members depending on the object implementation.

Yes, this is a memory leak.

Why would you alter data (set it to nullptr) in a destructor?

From my copy paste example. I fixed thank you.

Why 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?

I 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.

&& 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".

"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

Go has it bad - the bread-and-butter append() may or may not alias the original array. Even C++ balks at non-deterministic aliasing.

Nicolai Josuttis is writing a (not yet completed) book to it. I plan to read it. Maybe then it can be "simple".


This 20min Chrome Dev video goes over the same topic https://youtu.be/UNJrgsQXvCA

It'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?

This 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.

But the language is garbage collected, that's not desirable in the domain c++ operates

Rust completely dropped GC support in 0.12 — which was released on 9th October, 2014. That happened more than 5 years ago.

It'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.


> 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


> 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


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....


On 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.

There is a̵n̵ ̵a̵p̵p̵ Rust for that.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact