[HN Gopher] C++ std::move doesn't move anything: A deep dive int...
       ___________________________________________________________________
        
       C++ std::move doesn't move anything: A deep dive into Value
       Categories
        
       Author : signa11
       Score  : 221 points
       Date   : 2026-01-09 09:01 UTC (2 days ago)
        
 (HTM) web link (0xghost.dev)
 (TXT) w3m dump (0xghost.dev)
        
       | shmerl wrote:
       | I always understood move as moving ownership, so it's not a
       | misnomer.
       | 
       |  _> std::move is like putting a sign on your object "I'm done
       | with this, you can take its stuff."_
       | 
       | Which exactly is moving ownership.
        
         | cocoto wrote:
         | Personally I see std::move more like removing ownership because
         | it's not explicit from its call where the ownership is
         | transferred.
        
           | tsimionescu wrote:
           | Even that is a bit suspect, because ownership may well remain
           | with you even after the call, so it's not really removed.
           | 
           | For example, this is perfectly valid C++, and it is
           | guaranteed to have no issue:                 std::string abc
           | = "abc";       std::move(abc); //doesn't remove ownership or
           | do anything really       std::print(abc); //guaranteed to
           | print "abc"
        
         | vlovich123 wrote:
         | Std move doesn't move ownership. It simply casts into something
         | that could have its ownership taken. Whether or not that
         | actually happens is impossible to identify statically and the
         | value after ownership is consumed is unspecified - sometimes
         | it's UB to access the value again, sometimes it's not.
        
           | shmerl wrote:
           | May be disown would be more descriptive, but the point is
           | that it's intended for transferring of ownership versus
           | copying data.
        
             | masklinn wrote:
             | > it's intended for transferring of ownership versus
             | copying data.
             | 
             | It's intended for transferring ownership, but what it
             | actually does is mark the value as transferrable, whether
             | or not the value is actually transferred is up to the
             | callee.
        
           | mgaunard wrote:
           | That's quite inaccurate.
           | 
           | It needs to remain destructible, and if the type satisfies
           | things like (move-)assignable/copyable, those still need to
           | work as well.
           | 
           | For boxed types, it's likely to set them into some null
           | state, in which case dereferencing them might be ill-formed,
           | but it's a state that is valid for those types anyway.
        
             | vlovich123 wrote:
             | Well it's unspecified what empty/size return for
             | collections after a move. Not a dereference, not UB but
             | unspecified as I said. UB pops up in hand written code -
             | I've seen it and the language doesn't provide any
             | protection here.
             | 
             | Thankfully clippy lints do exist here to help if you
             | integrate that tooling
        
           | tsimionescu wrote:
           | It is absolutely knowable statically if ownership will be
           | taken. It's not necessarily very easy to do so, but the
           | decision is 100% up to the compiler, as part of overload
           | resolution and optimization choices (like the NRVO analysis
           | that the article mentions). Since ownership is an inherently
           | static concept, it doesn't even make sense to think about
           | "runtime ownership".
        
             | adrianN wrote:
             | My function can choose to move or not to move from an
             | object based on io input.
        
               | tsimionescu wrote:
               | Can you show an example of what you mean?
               | 
               | My claim is that, if I call `foo(std::move(myObj))`, it
               | is statically knowable if `foo` receives a copy of
               | `myObj` or whether it is moved to it. Of course, `foo`
               | can choose to further copy or move the data it receives,
               | but it can't choose later on if it's copied or not.
               | 
               | Now, if I give `foo` a pointer to myObj, it could of
               | course choose to copy or move from it later and based on
               | runtime info - but this is not the discussion we are
               | having, and `std::move` is not involved from my side at
               | all.
        
               | masklinn wrote:
               | > Can you show an example of what you mean?
               | void foo(std::unique_ptr<int, Deleter>&& p) {
               | std::random_device rdev {};             auto dist =
               | std::uniform_int_distribution<>(0, 1);             if
               | (dist(rdev)) {                 auto pp = std::move(p);
               | }         }
        
               | tsimionescu wrote:
               | This is exactly what I meant as irrelevant.
               | 
               | If I call `foo(std::move(my_unique_ptr))`, I know for
               | sure, statically, that my_unique_ptr was moved from, as
               | part of the function call process, and I can no longer
               | access it. Whether `foo` chooses to further move from it
               | is irrelevant.
        
               | masklinn wrote:
               | The only thing that is statically known here is that
               | you're wrong. The function I posted only moves its
               | parameter half the time, at random. _You_ may want to
               | _treat_ it as moved-from either way, but factually that's
               | just not what is happening.
        
               | charcircuit wrote:
               | This is like trying to defend that you can't statically
               | know the result of 1 + 2 because:                 void
               | foo() {         std::random_device rdev {};         auto
               | dist = std::uniform_int_distribution<>(0, 1);         if
               | (dist(rdev)) {           int res = 1 + 2;         }
               | }
               | 
               | I can tell you for sure that the result of 1 + 2 will be
               | 3.
        
               | masklinn wrote:
               | > This is like trying to defend that you can't statically
               | know the result of 1 + 2
               | 
               | It is completely unlike that. tsimionescu is asserting
               | that they can always know statically whether `foo` will
               | move its parameter. The function I provided is a counter-
               | example to that assertion.
               | 
               | Of course the branch body always moves, that's what it's
               | there for. That has no bearing on the argument.
        
               | charcircuit wrote:
               | >Of course the branch body always moves
               | 
               | >That has no bearing on the argument.
               | 
               | That is the whole argument. Let me quote the other
               | person: "My claim is that, if I call
               | `foo(std::move(myObj))`, it is statically knowable if
               | `foo` receives a copy of `myObj` or whether it is moved
               | to it."
               | 
               | It is saying that for "auto pp = std::move(p);" we will
               | know if it uses the move assign constructor or the copy
               | assign constructor.
        
               | masklinn wrote:
               | > That is the whole argument
               | 
               | No, it is not.
               | 
               | > Let me quote the other person: "My claim is that, if I
               | call `foo(std::move(myObj))`, it is statically knowable
               | if `foo` receives a copy of `myObj` or whether it is
               | moved to it."
               | 
               | Yes. `foo`.
               | 
               | > It is saying that for "auto pp = std::move(p);" we will
               | know if it uses the move assign constructor or the copy
               | assign constructor.
               | 
               | `pp` is not `foo`. That `pp` uses a move constructor is
               | not the subject of the debate.
               | 
               | You can literally take the function I posted, build a bit
               | of scaffolding around it, and observe that whether the
               | parameter is moved into `foo` or not is runtime
               | behaviour: https://godbolt.org/z/jrPKhP35s
        
               | knorker wrote:
               | No: https://godbolt.org/z/d7f6MWcb5
               | 
               | Look, the act of calling std::move and and calling a
               | function taking an rvalue reference in no way invokes a
               | move constructor or move assignment. It does not "move".
               | 
               | It's still just a reference, albeit an rvalue reference.
               | std::move and the function shape is about the type
               | system, not moving.
               | 
               | (Edit: amusingly, inside the callee it's an lvalue
               | reference, even though the function signature is that it
               | can only take rvalue references. Which is why you need
               | std::move again to turn the lvalue into rvalue if you
               | want to give it to another function taking rvalue
               | reference)
               | 
               | I didn't reply to this thread until now because I thought
               | you may simply be disagreeing about what "move" means (I
               | would say move constructor or move assignment called),
               | but the comment I replied to makes a more straightforward
               | factually incorrect claim, that can easily be shown in
               | godbolt.
               | 
               | If you mean something else, please sketch something up in
               | godbolt to illustrate your point. But it does sound like
               | you're confusing "moving" with rvalue references.
               | 
               | Edit: for the move to happen, you have to actually move.
               | E.g. https://godbolt.org/z/b8M495Exq
        
               | tialaramex wrote:
               | Yeah no.
               | 
               | In _Rust_ if you pass say a Box <Goose> (not a reference,
               | the actual object) into a function foo, it's gone,
               | function foo might do something with that boxed goose or
               | it might not, but it's gone anyway. If a Rust function
               | foo wanted to give you it back they'd have to return the
               | Box<Goose>
               | 
               | But C++ doesn't work that way, after calling foo
               | my_unique_ptr is guaranteed to still exist, although for
               | an actual unique_ptr it'll now be "disengaged" if foo
               | moved from it. It has to still exist because C++ 98 (when
               | C++ didn't have move semantics) says my_unique_ptr always
               | gets destroyed at the end of its scope, so newer C++
               | versions also destroy my_unique_ptr for consistency, and
               | so it must still exist or that can't work.
               | 
               | Creating that "hollowed out" state during a "move"
               | operation is one of the many small leaks that cost C++
               | performance compared to Rust.
        
               | knorker wrote:
               | You should not be downvoted, which you appear to be. Your
               | comparison is both correct and interesting.
               | 
               | Maybe you're being too verbose for your point, and it
               | would help readers if you summarize and narrow the
               | argument to:
               | 
               | In Rust a function signature can force a move to happen
               | at call time (by being non-reference and not Copy), but
               | in C++ a function taking rvalue reference (&&) only
               | signals the callee that it's safe to move if you want, as
               | it's not an lvalue in the caller.
               | 
               | It's an added bonus that Rust prevents reusing the named
               | variable in the caller after the move-call, but it's not
               | what people seem to be confused about.
        
               | vlovich123 wrote:
               | aside from what others wrote, it's also non local -
               | whether std::move even does anything is dependent on the
               | signature of foo - if foo takes it by const& you may
               | think you've transferred ownership when it hasn't
               | actually happened.
        
               | masklinn wrote:
               | That is static though, that `foo` takes its parameter by
               | `const&` and will thus not move it is available to the
               | compiler (or other tooling) at compile time.
               | 
               | The point of contention is whether that is always the
               | case, or whether there are situations where moving from
               | the parameter is a runtime decision.
        
               | ben-schaaf wrote:
               | No, it is not statically knowable if it is actually
               | moved.                   void foo(Obj && arg) {}
               | 
               | Does not move `arg`. It's fairly easy to write code that
               | assumes `std::move` moves the value, but that can lead to
               | bugs. For example:                   void
               | some_function(std::vector<int> &&);         void
               | some_function2(std::vector<int> &&);              void
               | main() {             std::vector<int> a = { 1 };
               | some_function(std::move(a));
               | a.push_back(2);
               | some_other_function(std::move(a));         }
               | 
               | The expectation is that `some_other_function` is always
               | called with `{ 2 }`, but this will only happen if
               | `some_function` _actually_ moves `a`.
        
               | adrianN wrote:
               | Is pushing to a moved-from vector even legal? I thought
               | in general the only guarantee you have after a move is
               | that is save to destruct the object.
        
             | charcircuit wrote:
             | I don't understand the downvoted here. Either the compiler
             | emits the code to call a move constructor or it doesn't.
        
               | Maxatar wrote:
               | Static analysis is about proving whether the code emitted
               | by a compiler is actually called at runtime. It's not
               | simply about the presence of that code.
               | 
               | Code can be emitted but never executed.
        
               | charcircuit wrote:
               | >Static analysis is about proving whether the code
               | emitted by a compiler is actually called at runtime.
               | 
               | That is but one thing that can static analysis can prove.
               | It can also prove whether source code will call a move
               | contractor or a copy constructor. Static analysis is
               | about analyzing a program without actually running it.
               | Analysizing what code is emitted is one way a program can
               | be analyzed.
        
           | knorker wrote:
           | After moving a value, it needs to remain in a "valid but
           | unspecified state".
           | 
           | How do you mean accessing a valid object is UB?
        
             | drysine wrote:
             | >After moving a value, it needs to remain in a "valid but
             | unspecified state".
             | 
             | No, it doesn't.
             | 
             | The standard library requires that for its classes, but not
             | the language.
             | 
             | "Unless otherwise specified, such moved-from objects shall
             | be placed in a valid but unspecified state."[0]
             | 
             | [0] https://timsong-
             | cpp.github.io/cppwp/n4950/lib.types.movedfro...
        
               | knorker wrote:
               | Ok, fair enough.
               | 
               | So you're saying if you use the language to write UB,
               | then you get UB?
               | 
               | Seems kinda circular. Ok, you're not the same user who
               | said it can be UB. But what does it then mean to same
               | "sometimes it's UB" if the code is all on the user side?
               | 
               | "Sometimes code is UB" goes for all user written code.
        
               | drysine wrote:
               | I mean the language doesn't dictate what post-condition
               | your class has for move-ctor or move-assignment.
               | 
               | It could be
               | 
               | - "don't touch this object after move" (and it's UB if
               | you do) or
               | 
               | - "after move the object is in valid but unspecified
               | state" (and you can safely call only a method without
               | precondition) or
               | 
               | - "after move the object is in certain state"
               | 
               | - or even crazy "make sure the object doesn't get
               | destroyed after move" (it's UB if you call delete after
               | move or the object was created on the stack and moved
               | from).
               | 
               | But of course it's a good practice to mimic the standard
               | library's contract, first of all for the sake of
               | uniformity.
        
             | masklinn wrote:
             | "Validity" is an extremely low bar in C++, it just means
             | operations with no preconditions are legal, which in the
             | most general case may be limited to destruction (because
             | non-destructive moves means destruction must always be
             | possible).
        
         | tsimionescu wrote:
         | _std::move_ itself doesn 't move ownership, though. It allows
         | the compiler to transfer ownership to the receiver of the
         | value, but it doesn't force it in any way. This is important,
         | because it means YOU may still be the owner of a value even
         | after you called _std::move_ on it.
         | 
         | Not to mention, ownership in C++ is not entirely lost with
         | moves in the traditional sense. For example, your code still
         | has to destruct the object even if you did move it to somewhere
         | else.
        
         | HarHarVeryFunny wrote:
         | Well, no, because CAN take isn't the same as WILL take.
         | 
         | Changing something to an rvalue means it'll now match a move
         | constructor, but there is no guarantee a move constructor will
         | be used, even if defined, because you've got classes like
         | std::vector that are picky and are explicitly looking for a
         | noexcept move constructor.
        
           | fluoridation wrote:
           | In that sense, std::move() is no different than other passing
           | semantics. Just because you wrote at the call site that you
           | want to pass a copy of your object doesn't mean that the
           | callee will actually make a copy of it.
        
             | HarHarVeryFunny wrote:
             | I'm not sure what you are saying.
             | 
             | If we have foo(std::string a, std string b), and then call
             | it like this:
             | 
             | std::string x;
             | 
             | std::string y;
             | 
             | foo(std::move(x), y);
             | 
             | Then x will be moved into a, and y will be copied into b.
             | 
             | The callee has no say in this - it's just the compiler
             | implementing the semantics of the language.
        
               | fluoridation wrote:
               | Who says there's only one resolution candidate? A
               | different overload could be defined elsewhere that the
               | compiler prefers for that particular combination of
               | arguments, that doesn't cause a copy. std::move() works
               | the same way. The semantics of the operation is defined
               | not by what's at the call site, but by the context.
        
               | HarHarVeryFunny wrote:
               | Sure overload resolution happens first, but once the
               | compiler has found the correct match then the way
               | arguments are passed depends only on the function
               | signature of that match (callee), and how the caller is
               | passing.
               | 
               | An argument passed to a value parameter will be passed by
               | copying, unless it's an rvalue (e.g. forced with
               | std:move) where a move constructor has been defined for
               | that type, in which case it will be moved. The callee has
               | no say in this.
        
               | fluoridation wrote:
               | >Sure overload resolution happens first, but once the
               | compiler has found the correct match then the way
               | arguments are passed depends only on the function
               | signature of that match (callee), and how the caller is
               | passing.
               | 
               | Yes, and std::move() works exactly the same. The compiler
               | first determines whether to move or to copy, and then
               | generates the call to the corresponding constructor or
               | assignment operator. Just like how foo(x) doesn't tell
               | you anything about whether a value is being copied,
               | foo(std::move(x)) doesn't tell you anything about whether
               | a value is being moved.
               | 
               | You might say "well, you need to look at all the
               | signatures of foo() to tell if there's a copy", and to
               | that I say, "yeah, and you need to look at what x is to
               | tell if there's a move".
        
               | HarHarVeryFunny wrote:
               | Not sure how this relates to your original claim, below,
               | that I have been responding to?
               | 
               | > Just because you wrote at the call site that you want
               | to pass a copy of your object doesn't mean that the
               | callee will actually make a copy of it.
        
               | fluoridation wrote:
               | Meaning, just like move semantics, overload resolution
               | can sometimes be surprising, and the compiler may not
               | always do what you expected if you don't fully understand
               | the types you're working with. std::move() is not special
               | in this sense.
        
               | HarHarVeryFunny wrote:
               | Ah, yes, unfortunately the language says that a value
               | parameter of type T and an rvalue parameter of type T
               | (T&&) are both equal priority "exact matches" to a call
               | passing an rvalue argument, but at least if that was the
               | only difference between two functions you'd get an
               | ambiguous overload compilation error rather than it just
               | selecting an unexpected one.
               | 
               | A workaround for this, if you want an rvalue parameter to
               | match an rvalue argument during overload resolution, is
               | to make the alternate "value" (vs rvalue) overload a
               | const reference argument vs a value one.
               | 
               | So, if you have f(T&&) and f(T), and call f(std::move(t))
               | then you'll get an ambiguous overload compilation error,
               | but if you instead had f(T&&) and f(const T&), then
               | f(std::move(t)) will match the rvalue one as you may hope
               | for.
        
         | usefulcat wrote:
         | std::allow_move probably would have been a more accurate name
         | for std::move.
        
       | stingraycharles wrote:
       | I don't think this is particularly insightful, as move semantics
       | and r-values are higher level language semantics, nothing more
       | and nothing less.
       | 
       | Rust's borrow checker doesn't actually borrow anything either,
       | it's operating on a similar level of abstraction.
        
         | masklinn wrote:
         | > Rust's borrow checker doesn't actually borrow anything either
         | 
         | Why would it? It's called the borrow _checker_ , not the
         | borrower. So it checks that your borrows are valid.
         | 
         | std::move looks and feels like a function, but it doesn't do
         | what it _says_ , it makes objects movable but does never moves
         | them (that's up to whatever is using the value afterwards). If
         | you want something similar in Rust, Pin is a much better
         | candidate.
        
           | vouwfietsman wrote:
           | Sure, but from the perspective of the code that has the
           | move() its good to assume the value _is moved_ at that call,
           | which I guess was the intention of picking the name.
        
             | dathinab wrote:
             | yes
             | 
             | std::move tells the devs and the compiler that you _intend_
             | the value to be moved
             | 
             | sadly that isn't reflected well in it's implementation as
             | it will "silently" degrade even if it isn't a "move" (1)
             | 
             | A `std::move` which fails to compile if it can't be a
             | move(1) it would not have this issues.
             | 
             | But it has other issues, mainly wrt. library design
             | especially related to templates/generics, which probably(?)
             | need a `std::move` which works like the current one. I
             | think someone else in this comment section already argued
             | that one issue with modern C++ is too much focusing on the
             | complicated/template/const library design by experts case
             | compared to the "day to day" usage by non experts.
             | 
             | (1): There is a bit of gray area in what in rust would be
             | Copy types, for simplicity we can ignore them in this
             | hypothetical argument about an alternative std::move
             | design.
        
             | masklinn wrote:
             | Usually yes, however because that's not for some resource
             | types it can lead to less than ideal behaviour e.g. if your
             | RAII resource is something which will get corrupted if
             | there are two handles to it (some sort of odd hardware
             | resource), you std::move() the object into a callee, assume
             | it is moved and released, so you acquire a new resource,
             | and turns out the callee did not move it and now you have
             | two of them.
        
       | fooker wrote:
       | Maybe std::make_movable would have been a slightly better name,
       | but it's so much simpler to write std::move.
        
         | krior wrote:
         | thanks to the incredible advances in terms of developer tooling
         | over the last 50 years (i.e. tab-autocompletion) there should
         | be no difference in writing those two.
        
           | kaashif wrote:
           | There is a difference, lots of stuff starts with make_, so
           | lots of possible completions.
        
         | magicalhippo wrote:
         | Split the difference with std::moveable().
         | 
         | Also signals it doesn't actually move, while remaining just as
         | fast to type.
        
           | fooker wrote:
           | std::movable is a concept now!
           | 
           | https://en.cppreference.com/w/cpp/concepts/movable.html
        
         | pseidemann wrote:
         | std::rvalue
        
         | bitexploder wrote:
         | But that misses too much of the semantics. It also implies
         | ownership transfer, even if copied.
        
       | zabzonk wrote:
       | Naming things is hard.
        
         | pseidemann wrote:
         | I'm convinced naming things is equivalent to choosing the right
         | abstraction, and caching things is creating a correct "view"
         | from given normalized data.
        
       | oezi wrote:
       | The best way to think about it is that std::move is a cast.
       | 
       | https://stackoverflow.com/a/42340735
        
         | ahoka wrote:
         | The article explains this.
        
       | rurban wrote:
       | Should have be called give(). But naming things correctly is
       | hard, and the C++ committee is known to do a lot of things
       | incorrectly
        
         | masklinn wrote:
         | That has about the same issue: like std::move it doesn't really
         | explain that the receiver decides.
        
           | vouwfietsman wrote:
           | std::offer
        
             | repelsteeltje wrote:
             | [?] That's actually quite accurate. I like it!
        
             | spot5010 wrote:
             | Maybe a std::take to pair with it?
        
             | HarHarVeryFunny wrote:
             | std::abandon() ?!
        
         | pseidemann wrote:
         | There is no giving (or taking).
         | 
         | I think std::rvalue would be the least confusing name.
        
         | usrnm wrote:
         | The name predates the standardisation. The committee did not
         | come with the whole thing themselves, rather they adopted and
         | expanded already existing library implementations. You could
         | move in C++, with this exact name, long before C++11.
         | 
         | See, for example, this implementation
         | https://stlab.adobe.com/group__move__related.html
        
           | tialaramex wrote:
           | Howard Hinnant's original move proposal for C++ is from 2002.
           | And by then even the destructive move (the more useful
           | operation and the semantic provided in Rust) was well
           | understood.
           | 
           | Hinnant said they couldn't find a way to do destructive move
           | and have the C++ inheritance hierarchy. To me it's obvious
           | what loses in this case, but to a C++ programmer at the turn
           | of the century apparently C++ implementation inheritance ("OO
           | programming") was seen as crucial so C++ 11 move semantics
           | are basically what's described in that proposal.
        
         | jsphweid wrote:
         | std::movable
        
       | MORPHOICES wrote:
       | Value categories and move semantics are great examples of
       | programming concepts that can cause confusion, and it's a great
       | example of how not having a bad documentation can still lead to
       | confusion through bad mental models. ~
       | 
       | Intuitively you think you understand what is going on, and you
       | think you can answer what is going on, and you can even use it
       | due to understanding it on an operational level, but you can't
       | explain it due to your confusion.
       | 
       | As a result, you most likely are going to create a lot of small
       | bugs in your software and a lot of code that you don't really
       | understand. So, I'm curious to know what others think.
       | 
       | What concept did you learn later than you thought you would? What
       | knowledge did you struggle with the most? What finally helped you
       | understand it?
        
         | meindnoch wrote:
         | Coming from other languages with generics, it took a while for
         | me to internalize SFINAE when writing templated code.
        
           | spacechild1 wrote:
           | Luckily, with C++17's if-constexpr and C++20's concepts,
           | SFINAE has become mostly obsolete for new C++ code (unless
           | you have/want to support older C++ standards).
        
         | porise wrote:
         | Value categories actually just are confusing in a language as
         | complicated as C++. I'm not willing to bet that even senior C++
         | developers are always going to be able to deduce the correct
         | value category.
         | 
         | And worse, in typical C++ fashion, there is still little
         | guaranteed as far as when std::move will actually cause a move.
         | The implementation is still given a lot of leeway. I've been
         | surprised before and you basically have no choice but to check
         | the assembly and hope it continues to be compiled that way as
         | minor changes make their way into the code base.
        
           | nickelpro wrote:
           | > even senior C++ developers are always going to be able to
           | deduce the correct value category
           | 
           | Depends what "senior" means in this context. Someone with
           | 20-years of domain experience in utility billing, who
           | happened to be writing C++ for those 20 years? Probably not.
           | 
           | Someone who has been studying and teaching C++ for 20 years?
           | Yes they are able to tell you the value category at a glance.
           | 
           | Language experience is not something you develop
           | accidentally, you don't slip into just because you're using
           | the language. Such tacit experience quickly plateaus. If you
           | make the language itself the object of study, you will
           | quickly surpass "mere" practitioners.
           | 
           | This is true of most popular programming languages in my
           | experience. I find very, very few Python programmers
           | understand the language at an implementation level, can
           | explain the iterator protocol or what `@coroutine` actually
           | used to do, how `__slots__` works, etc.
           | 
           | C++ is not unique in this, although it is old and has had a
           | lot more time to develop strange corners.
        
         | HarHarVeryFunny wrote:
         | The issue TFA is describing isn't really about not
         | understanding move semantics, it's about not having read the
         | documentation for the STL container classes, and not therefore
         | realizing that anything requiring reallocation needs a noexcept
         | move constructor (else will fall back to copy construction).
         | 
         | Note that a move constructor that is NOT declared with noexcept
         | is perfectly valid, and will happily be used most of the time
         | (other than where code, such as the STL, is explicitly looking
         | for a noexcept one).
         | 
         | So, for example:
         | 
         | HeavyObject t;
         | 
         | HeavyObject s(std::move(t));
         | 
         | Will cause t to be moved to s.
        
       | krona wrote:
       | > So the standard library plays it safe: if your move constructor
       | might throw (because you didn't mark it noexcept), containers
       | just copy everything instead. That "optimization" you thought you
       | were getting? It's not happening.
       | 
       | This is a bit of a footgun and clang-tidy has a check for it:
       | performance-noexcept-move-constructor. However, I don't think
       | it's enabled by default!
        
         | juliangmp wrote:
         | Most sensible Compiler flags aren't enabled by default... I
         | keep a list of arguments for gcc to make things better, but
         | even then you'll also wanna use a static analysis tool like
         | clang-tidy
        
           | teraro wrote:
           | Would you mind sharing your list?
        
         | Fiveplus wrote:
         | The reason performance-noexcept-move-constructor is not enabled
         | by default is likely because blindly applying noexcept is
         | dangerous if the underlying logic isn't actually exception-
         | free. If you let clang-tidy slap noexcept on a move constructor
         | that does end up throwing (perhaps because it calls into a
         | legacy member or allocates memory internally), the runtime
         | behavior changes from caught exception to std::terminate().
        
           | phkahler wrote:
           | Exceptions should never be enabled by default. We live in a
           | 64bit world so allocations failing indicates some other
           | problem.
        
             | zbentley wrote:
             | What does processor but width have to do with the
             | likelihood of allocation failures?
        
               | petcat wrote:
               | 640K ought to be enough for anybody!
        
               | HarHarVeryFunny wrote:
               | I think what he means is that on a 64-bit system you have
               | a massive virtual address space (typically only 48-bit,
               | but that's still 256TB), and since malloc allocates from
               | virtual address space, not limited by physical memory, it
               | is unlikely you will get a malloc failure (unless you are
               | trying to allocate more than 256TB per process, maybe due
               | to a memory leak).
        
             | usefulcat wrote:
             | Exceptions can be used to indicate many kinds of errors,
             | not just allocation failures.
        
           | immibis wrote:
           | clang-tidy checks but doesn't change things for you.
           | 
           | Since you can also put noexcept(false) to indicate something
           | throws exceptions and you didn't just forget to mark it
           | noexcept, it's not a bad policy to say every move constructor
           | should have a noexcept marker.
        
           | dbcpp wrote:
           | I would argue performance-noexcept-move-constructor should
           | always be on. Move constructors should almost always be
           | noexcept since they typically just move pointers around and
           | don't do allocations normally.
        
             | jcelerier wrote:
             | eh, depends. for instance think about a small_vector or
             | small_string
        
               | dbcpp wrote:
               | True, in that case it should just adopt the noexcept
               | status of the object it holds.
        
           | HarHarVeryFunny wrote:
           | The documentations seems to say that option only causes the
           | compiler to issue a warning when move constructors are not
           | marked noexcept - it doesn't override anything.
           | 
           | https://clang.llvm.org/extra/clang-
           | tidy/checks/performance/n... constructor.html
           | 
           | Note that the way std::vector (and other STL containers)
           | require noexcept move constructors for reallocation is by
           | using template matching, and of course any other code might
           | be doing this too, so having a compiler option that forced a
           | constructor (or anything) to have a type signature different
           | than the way it was declared would be a pretty dangerous
           | thing to do since it'd be hard to know what the consequences
           | would be.
        
         | rfc3092 wrote:
         | performance-noexcept-move-constructor is great but it also
         | complains about move assignment operators, which are completely
         | different beasts and are practically impossible to make
         | noexcept if your destructors throw.
        
           | dataflow wrote:
           | If that's the issue you're facing, consider clang-query,
           | e.g.: https://godbolt.org/z/bfG94qGan                 match c
           | xxConstructExpr(hasDeclaration(cxxConstructorDecl(isMoveConst
           | ructor(), unless(isNoThrow())).bind("throwing-move")))
           | 
           | You can put extra constraints on the caller if you'd like
           | (e.g., isInStdNamespace()), though it's less trivial. Happy
           | to help write something if you have a precise idea of what
           | you want to match.
        
           | beached_whale wrote:
           | Throwing destructors will generally end in termination of the
           | program if they are used as class members. Types like
           | scope_exit are fine, but anywhere else will probably have
           | noexcept(true) on it's destructor.
        
         | beached_whale wrote:
         | Throwing move is super weird too. I believe that it was a
         | mistake to not treat user move like C++11 destructors and
         | default to noexcept(true) on them. But it is what it is.
         | 
         | On the other hand, writing special member functions at all(move
         | & copy constructor/assignment, destructor) is a smell for types
         | that don't just manage the lifetime of an object(unique_ptr
         | like things). People should not generally be writing them and
         | being open to the mistake of getting noexcept wrong.
        
         | grogers wrote:
         | If I'm not mistaken, all the pitfalls in the article have
         | clang-tidy lints to catch
        
         | jeffbee wrote:
         | Nothing about clang-tidy is enabled by default, and getting it
         | to run at all in realistic projects is quite a chore.
        
       | drob518 wrote:
       | About 28 years ago, I figured out that I'm just not smart enough
       | to use C++. There are so many foot guns and so much rampant
       | complexity that I can't keep it all straight. I crave simplicity
       | and it always felt like C++ craved the opposite.
        
         | groundzeros2015 wrote:
         | Same. I've read all the books. Written all these things at
         | least a few times. It's just not doable post C++11.
        
         | benreesman wrote:
         | Systems programming in the large is hard, owning the category
         | for decades harder still.
         | 
         | Even languages that have tried to fast-follow and disrupt C++
         | end up looking a lot like C++. There is an irreducible
         | complexity.
        
           | zbentley wrote:
           | I hear this a lot, but I don't really understand how this
           | manifests in language complexity like the stuff in TFA in
           | practice.
           | 
           | Like, I can understand how systems programming requiring
           | programmers to think about questions like "how can I proceed
           | if allocation fails? How does this code work in an embedded
           | context with no heap?" is hard and irreducible.
           | 
           | But I can't understand why a language's choice to impose
           | complex rules like C++ move constructor hell is an inevitable
           | outcome of irreducible complexity in systems programming. Put
           | another way: C is also a systems programming language that
           | works for many people, and it doesn't have any of these
           | Byzantine rules (unless you build them yourself). That's not
           | to say C is better/preferable, but it swims in the same
           | "official Big Gun systems language" pond as C++, which seems
           | to indicate that revalue semantics as complex as C++'s are a
           | choice, not an inevitability.
        
             | cjfd wrote:
             | The difference is that in C one is supposed to do
             | allocations and deallocations oneself. Then move semantics
             | is just pointer assignment with, of course, the catch that
             | one should make sure one does not do a double-free because
             | ownership is implicit. In C++ ownership is indicated by
             | types so one has to write more stuff to indicate the
             | ownership.
        
               | SJC_Hacker wrote:
               | > The difference is that in C one is supposed to do
               | allocations and deallocations oneself
               | 
               | No, you should only use the heap if necessary.
               | 
               | The bigger issue in C is there is no concept of
               | references, so if you want to modify memory, the only
               | recourse is return-by-value or a pointer. Usually you see
               | the latter, before return value optimization it was
               | considered a waste of cycles to copy structs.
               | 
               | In the embedded world, its often the case you won't see a
               | single malloc/free anywhere. Because sizes of inputs were
               | often fixed and known at compile time for a particular
               | configuration.
        
             | HarHarVeryFunny wrote:
             | I wouldn't say issues like this are dues to irreducible
             | complexity, but more symptomatic of long-lived languages
             | that continually get extended but don't give up on
             | backwards compatibility. It's basically the 2nd law of
             | thermodynamics applied to programming languages that they
             | will eventually die due to increased entropy.
             | 
             | Maybe if move semantics, and noexcept, had been designed
             | into C++ from the beginning then the designers might have
             | chosen to insist that move constructors be noexcept, but
             | since these were added later there is code out there with
             | move constructors that do throw exceptions...
             | 
             | Note by the way that the issue being described isn't
             | strictly about std::move or move semantics in general, but
             | more about the STL and containers like std::vector that
             | have chosen to define behavior that makes noexcept move
             | constructors necessary to be used when reallocating.
        
             | kanbankaren wrote:
             | > But I can't understand why a language's choice to impose
             | complex rules like C++ move constructor hell is an
             | inevitable outcome of irreducible complexity in systems
             | programming.
             | 
             | Programmer here for 30 years in C/C++. It is true that C++
             | has become a more complex language after rvalue references
             | were introduced, but you have to understand the rationale
             | behind C++: a language suitable for large scale systems
             | programming with *ZERO OVERHEAD*.
             | 
             | The language complexity especially rvalue references was to
             | reduce overhead. Pre-C++-11, there were many code patterns
             | that involved constructing temporaries and destroying them
             | immediately.
             | 
             | C is not suitable as a large scale programming language.
             | Just look at the number of defects in the Linux kernel and
             | their attempt at extending the language through custom
             | compiler attributes to overcome the limitations of C.
        
               | LexiMax wrote:
               | > but you have to understand the rationale behind C++: a
               | language suitable for large scale systems programming
               | with _ZERO OVERHEAD_.
               | 
               | Is this the reason why C++ was created, or the last
               | remaining niche that C++ is holding onto?
               | 
               | I remember the early 90's, and it very much seemed like
               | C++ was being pushed as both a general-purpose language
               | and the logical successor to C, insert Linus Torvalds
               | rant here. On top of that, C++ made the decision to
               | privilege a form of polymorphism that had pointer-chasing
               | baked into its internal design, as well as having a good
               | chunk of the standard library being considered a footgun
               | best to avoid due to how much it blew up compile-times.
               | 
               | I think that C++ is a zero-overhead language _now_
               | because a series of general purpose languages that came
               | afterwards took the other niches away from it, plus the
               | benefit of 30+ years worth of compiler optimizations that
               | were originally largely aimed at the mountain of C code
               | that was out there.
               | 
               | EDIT: Almost forgot about exceptions, the other enormous
               | performance footgun that was an early pre-standard C++
               | feature.
        
             | jesse__ wrote:
             | As you pointed out, the idea that a systems language
             | requires some high level of complexity is just straight-up
             | wrong, and demonstrably so (see, C).
             | 
             | The best programmers I know of have basically all abandoned
             | C++ in favor of either languages they made, or just use
             | plain C
        
             | usefulcat wrote:
             | > I can't understand why a language's choice to impose
             | complex rules like C++ move constructor hell is an
             | inevitable outcome of irreducible complexity in systems
             | programming
             | 
             | It's not about irreducible complexity in systems
             | programming, it's about irreducible complexity in the
             | creation of higher level abstractions.
             | 
             | You could certainly implement something functionally
             | equivalent to std::vector<std::string> in C. What you
             | couldn't do in C is implement std::vector<T> correctly and
             | efficiently for any type T. That's where much of the
             | complexity comes from.
             | 
             | The hard part is giving the compiler enough information so
             | that it can automate a lot of what would have to be
             | manually written in a language like C, and to produce a
             | result that is both correct _and_ efficient.
        
           | drob518 wrote:
           | I have no problem with systems programming issues. That
           | complexity is essential complexity inherent in the problem
           | itself, regardless of language. I have a problem with C++'s
           | accidental complexity. I find C much more tractable. It
           | certainly has a few of its own footguns, but it has much less
           | accidental complexity.
        
             | SJC_Hacker wrote:
             | As the author of the FQA noted (Yosef K-something), in C++
             | its more the combinations of features which causes so many
             | issues.
             | 
             | And here we see this principle rear its ugly head yet
             | again. In this case, its the combination of exceptions,
             | manual memory allocation and the desire to make things work
             | efficiently - of which the move constructor was developed
             | as a "solution"
        
           | CyberDildonics wrote:
           | What does in the large mean?
        
         | epx wrote:
         | I understand the individual rationales of C++ things but I lost
         | the faith on the whole thing.
        
           | chihuahua wrote:
           | The way C++ has developed over the past 20 years seems
           | similar to someone starting with an algorithm that fails for
           | some edge cases, and patching the behavior with a different
           | hack for each edge case, which breaks other cases, then
           | patching those, and on and on forever.
        
             | ryandrake wrote:
             | I think the way to be successful with C++ is to 1. Pick a
             | sensible subset of the language that you allow in your
             | project, and ban everything else. How much that subset
             | should include is a valid debate and reasonable people can
             | disagree, but I don't know of any successful C++ project
             | that just YOLOs every part of the language into the
             | project. And 2. (related) Pick the _earliest_ possible
             | standard that your team can live with, and don 't give in
             | to the temptation of cherry-picking anything from a future
             | standard. For instance, the decision of switching from
             | C++14 to C++17 should be a major debate full of
             | fistfighting.
        
         | FpUser wrote:
         | C++ is a universal tool with long history. So yes it makes it
         | very complex for various reasons. However it does not preclude
         | one from being productive. I do not come anywhere close to
         | being expert in C++. Still write software that blows the shit
         | out of competition. I have general understanding how the things
         | work and when I need some particular feature I just look up the
         | efficient way of doing it in whatever language. Not just for
         | C++. I actively use many languages. My goal is to deliver good
         | software and get paid by happy client, not to know every little
         | detail of the tools I use, it is just impossible and serves no
         | useful purpose.
        
         | lefty2 wrote:
         | c++ 03 was a lot easier.
         | 
         | For instance, if you want to avoid unnecessary copy operations
         | when returning a string, just return it in variable that you
         | pass by reference (eg. void doSomething(string& str);) likewise
         | avoid the vector class making unnecessary copies, simply by
         | creating the objects on the heap and use a vector of pointers
         | instead of values. It's a bit more ugly, but it works, and you
         | don't need to read a 24 page blog to understand all the corner
         | cases where it can go wrong. modern c++ is all about syntactic
         | suger.
        
           | usefulcat wrote:
           | Agreed that c++03 was much simpler, but that doesn't change
           | the fact that there are useful things that are possible in
           | modern c++ that simply were not possible before.
           | 
           | Like if I have a vector<std::string>, in c++03 when it
           | resizes it _must_ copy every string from the old storage to
           | the new storage. For a vector of size N, that 's up to N+1
           | allocations (allowing for the possibility that std::string
           | uses the small string optimization).
           | 
           | Granted, std::string doesn't _have_ to allocate when copied
           | if it 's a "copy on write" implementation. IIRC, there were
           | some implementations that used that technique when c++03 was
           | the latest, but I don't think there are any that still do,
           | due to other problems with COW.
           | 
           | In modern c++, that same vector resizing operation requires
           | exactly one allocation (for the new vector storage), because
           | all the strings can be _moved_ from the old storage to the
           | new.
           | 
           | Yes, you could have a vector of pointers to std::string, but
           | now you've got yet another allocation (and indirection on
           | access) for every string. In practice that tradeoff almost
           | never makes sense, unless perhaps the strings have shared
           | ownership (e.g. vector<shared_ptr<string>>).
           | 
           | Ultimately, I think there's really no question that the
           | vector resizing optimization described above is useful in
           | certain scenarios. Having said that, I do agree that the
           | associated complexity is annoying. Therefore, the real
           | question is whether it's possible to have these benefits with
           | less complexity, and I personally don't know the answer to
           | that.
        
         | fenwick67 wrote:
         | I write c++ for a living and I feel the same way. And many c++
         | codebases have that OOP AbstractObjectInterfaceFactory stink
         | which makes it even worse
        
       | Fiveplus wrote:
       | Regarding mistake 1: return std::move(local_var), it is worth
       | clarifying why this is technically a pessimization beyond just
       | breaking NRVO. It comes down to the change in C++17 regarding
       | prvalues.
       | 
       | > Pre-C++17, a prvalue was a temporary object.
       | 
       | > Post-C++17, a prvalue is an initializer. It has no identity and
       | occupies no storage until it is materialized.
        
         | HarHarVeryFunny wrote:
         | In C++17 and later, return std::move(local_variable) as opposed
         | to return local_variable is only breaking NRVO (which avoids
         | even having to move, by essentially replacing local_variable
         | with a reference to the variable the caller is assigning the
         | function result to).
         | 
         | In C++17 if you do return std::move(local_variable) it will do
         | exactly what you asked for and move the local variable to the
         | return value, which with copy elision means directly to the
         | caller's variable.
         | 
         | So, return std::move(local_variable) is only preventing NRVO,
         | it's not preventing a move (even though you shouldn't be asking
         | for a move, because move is not the most efficient way).
        
       | andyjohnson0 wrote:
       | > This code works. It compiles. It runs. But depending on how
       | you've implemented your types, it might be performing thousands
       | of expensive copy operations instead of cheap moves without you
       | realizing it.
       | 
       | I've spent the last two decades in the .net platform. But for a
       | decade or so before that I was a C++/Unix dev. I remember old
       | style "C with classes" C++ as being fairly small and elegant, and
       | approximately as easy to reason about as C# - albeit that you had
       | the overhead of tracking object ownership and deallocation.
       | 
       | What the language has become now, boggles my mind. I get hints of
       | elegance/power and innovation when I read about it, but the sheer
       | number of footguns is astonishing. I'm very sure that I'm not
       | clever enough to understand it.
       | 
       | But some very smart people have guided the language's evolution.
       | So, what are the forces that have determined the current state of
       | C++?
        
         | zabzonk wrote:
         | > So, what are the forces that have determined the current
         | state of C++?
         | 
         | A subset of the language aimed at library writers. As a user of
         | those libraries all these weirdo features are likely to be
         | transparent.
        
           | yosefk wrote:
           | TFA explains how std::move is tricky to use and this is not a
           | feature reserved for library writers
        
             | zabzonk wrote:
             | Of course it is not reserved for library writers - nothing
             | is. But it is not a feature that application writers should
             | worry about overmuch.
        
               | yosefk wrote:
               | std::move is definitely for there for optimizing
               | application code and is often used there. another silly
               | thing you often see is people allocating something with a
               | big sizeof on the stack and then std::moving it to the
               | heap, as if it saves the copying
        
               | zabzonk wrote:
               | > another silly thing you often see is people allocating
               | something with a big sizeof on the stack and then
               | std::moving it to the heap, as if it saves the copying
               | 
               | never seen this - an example?
        
         | ghosty141 wrote:
         | > So, what are the forces that have determined the current
         | state of C++?
         | 
         | I'm very confident that the main driving factors are:
         | 
         | 1. "performance" (not wanting to do more allocations than
         | necessary)
         | 
         | 2. abi compatibility
         | 
         | 3. adding features without caring how well they integrate
         | 
         | Example for 1:
         | 
         | "emplace", you normally have "append" but emplace directly
         | constructs the object in the container instead of having to be
         | constructed first and then moved into the container. This nice
         | and all but breaks when using pairs (for reasons you can google
         | but I don't wanna explain here). So now you have these obscure
         | classes like
         | https://en.cppreference.com/w/cpp/utility/piecewise_construc...
         | which solve this.
         | 
         | Example for 2:
         | 
         | Basically they never break the ABI and this leads to tons of
         | old stuff hanging around and never being changed and just more
         | stuff being added on top. std::iostream is famously slow and a
         | big reason is because you can't fix it without breaking the abi
         | which they don't wanna do.
         | 
         | Example for 3:
         | 
         | The whole template thing adds _so much_ complexity it 's
         | bonkers, I think c++ without templates would be pretty
         | manageable comparatively. For example because C++ has
         | constructors and they don't quite mix well with templates you
         | suddenly end up in the situation that you have 2 concepts:
         | "normal" template argument deduction and constructor template
         | argument deduction (CTAD). Because of this asymmetry you need a
         | custom language feature called "deduction guides" to maneuver
         | yourself out of the problems that come from this.
         | 
         | Or another short one: std::expected without something like the
         | "!" that rust has. You end up with endless
         | "if(result.has_value()) { return result; }" cascades and it's
         | horribly unergonomic. So now we have a Result class but it's
         | practically unusable that it will only fragment the ecosystem
         | even more.
        
           | drysine wrote:
           | >Example for 1: ...breaks when using pairs
           | 
           | No, it doesn't. But sometimes you want to construct pair's
           | elements in-place too and that's what piecewise_construct is
           | for.
        
         | pjmlp wrote:
         | Note that C# 14 versus C# 1.0 isn't suffering from feature
         | creap as well.
         | 
         | What has guided C++ are the 300+ volunteers that get to submit
         | papers, travel around the world attending the meetings, and win
         | the election rounds of what gets into the standard.
         | 
         | Unfortunately design by committee doesn't lead to a clear
         | product roadmap.
        
         | dathinab wrote:
         | > old style "C with classes" C++ as being fairly small and
         | elegant
         | 
         | it (C++) never really was that
         | 
         | but it was possible to use it "as if it where that" (kinda,
         | e.g. there is code which is valid in C but UB in C++)
         | 
         | I mean there where also times where books which told you that
         | in C everything "is just bits in memory" where
         | popular/believed/beloved, even through that never really was
         | true outside of some very specific cases (all of CPU without
         | caches, only in order execution, single core, a mostly non-
         | optimizing compiler, and other requirements). It was just that
         | the chance to run into issues was much less likely if you go
         | ~20+ years back into the past so you could kinda use it like
         | that (at some risk, especially wrt. forward compatibility).
         | 
         | Today you find ton of material even about obscure features,
         | complications, hidden food guns, etc. so things do look/feel
         | far more overwhelming IMHO.
         | 
         | That modern C++ is a bit like a different language glued on top
         | of old C++ doesn't exactly help either.
        
       | amelius wrote:
       | Sounds more like a contract thing. Of course std::move should be
       | able to throw exceptions (like when it runs out of memory), but
       | when it throws an exception it should still guarantee that memory
       | is in a consistent state.
       | 
       | So the fault here is with std::vector who didn't write that
       | contract.
        
       | groundzeros2015 wrote:
       | Before move semantics the HeavyObject problem was solved in most
       | cases by specializing std::swap for each container.
       | 
       | The design lesson I draw from this is that pursing a 100% general
       | solution to a real problem is often worse than accepting a crude
       | solution which covers the most important cases.
        
         | dathinab wrote:
         | my take looking at languages beyond C++ is a very different one
         | 
         | you want a well working general solution which works well (most
         | of the time for most of the "generic code" (i.e. good defaults
         | for the default use-case).
         | 
         | and then add escape hatches for micro-optimizations, micro-
         | control etc.
         | 
         | C++ on the other hand was deeply rooted designed with micro
         | optimizations and micro control first.
         | 
         | "Generic solutions" where then tried to be added on top, but
         | not by changing a badly working abstraction/design but by
         | adding more abstraction layers and complexity on top. And with
         | a high requirements for back/forward compatibility, not just
         | with the language but ton of different tooling. That this isn't
         | playing out well is kinda not really surprising IMHO. I mean
         | adding more abstraction layers instead of fixing existing
         | abstraction layers rarely plays out well (1) especially if the
         | things you add are pretty leaky abstractions.
         | 
         | -----
         | 
         | (1): In context of them archiving overall the same goal with
         | just different details and no clear boundaries. Layering very
         | different kind of layers is normal and does make sense in a lot
         | of situations. Just what C++ does is like layering "a generic
         | system programming language" (modern C++) on top of "a generic
         | system programming language" (old C++) without clear
         | boundaries.
        
           | groundzeros2015 wrote:
           | C++ does have reasonable defaults. You never have to worry
           | about move if you are using standard containers or
           | unique_ptr.
           | 
           | But eventually those escape hatches come bite you and you
           | need to worry about.
           | 
           | Complexity is inherent to the system. Wrapping it in a nice
           | interface doesn't make it go away.
           | 
           | ---
           | 
           | The problem I see is move semantics are a real thing in
           | programming languages where types can own resources.
           | 
           | Most languages just choose not to handle them well or limit
           | their feature set. For example swift tries to use copy on
           | write to avoid it
           | 
           | So eventually feature creep happens and you get
           | borrowing/move.
        
         | usefulcat wrote:
         | That still leaves the problem of when to use std::swap vs
         | ordinary assignment in generic (i.e. templated) code.
         | 
         | Like when std::vector needs to resize its underlying storage
         | (as a result of push_back, for example), it has to decide which
         | approach to use to copy/move items from the old storage to the
         | new storage.
         | 
         | For std::vector<std::string>, std::swap would probably be at
         | least ok if not optimal, but for std::vector<int> it would be
         | overkill and therefore decidedly non-optimal. In the latter
         | case, you want to do memcpy(new, old) and be done, not
         | std::swap(old[i], new[i]) for each int.
         | 
         | I think a lot of the motive for adding move semantics to c++
         | has to do with giving the compiler enough information to
         | produce results that are both optimal and correct in generic
         | code.
        
           | groundzeros2015 wrote:
           | If the type is trivial you don't swap, if it is you do.
           | 
           | There were already special cases for this in C++98 in order
           | to optimize for when memcpy and memove could be invoked.
        
       | cenamus wrote:
       | I found the previous discussion and article very helpful
       | 
       | https://news.ycombinator.com/item?id=45799157 (87 comments)
        
       | j1elo wrote:
       | > _[std::move silently copies const values, because] If something
       | is const, you can't move from it by definition._
       | 
       | Whoever wrote that definition should have a thing or two to learn
       | from Rust. Different language I know, but it proves that it
       | wasn't needed to cause so much confussion and collectively so
       | much time and performance lost.
       | 
       | Also, who writes rules like that and ends the day satisfied with
       | the result? It seems unlikely to feel content with leaving huge
       | footguns and being happy to push the Publish button. I'd rather
       | not ship the feature than doing a half-assed work at it.
       | Comparing attitudes on language development and additions, it
       | makes me appreciate more the way it's done for the Go lang, even
       | though it also has its warts and all.
        
         | pjmlp wrote:
         | There was no Rust in 2011.
        
           | j1elo wrote:
           | The point is not a comparison with Rust per-se, but the fact
           | that a better implementation of the idea was mathematically
           | and/or technically possible; and the personal opinion that
           | such huge footguns that the language accumulates over the
           | years are maybe signals of having needed more thought to them
           | before they were considered ready.
           | 
           | e.g. if something as simple of a inconspicuous std::move in
           | the wrong place can break the whole assumption about move
           | semantics, then make that impossible to do, or at least do
           | not make it the default happy path, before you consider it
           | production ready. What the heck, at the very least ensure it
           | will become a compiler warning?
           | 
           | Hence the mention to Go and how they follow exactly this path
           | of extending discussion as long as needed, even if it takes
           | 10 years, until a reasonable solution is found with maybe
           | small gaps, but never huge ones such as those explained in
           | this article (plus tens of others in any other text about the
           | language)
        
             | pjmlp wrote:
             | It took 13 years to get C++11, actually.
             | 
             | Go's discussion is interesting, given how much programming
             | language design history, and flaws of existing languages,
             | they ignore to this day.
        
               | j1elo wrote:
               | A bit more if we consider the "bugfixing" release that
               | was C++14 :)
               | 
               | But yeah it makes sense, given how that was the jumpstart
               | of the whole modernization of the language. I believe it
               | was a big undertake that required the time it took. Still
               | years have passed and footguns keep accumulating... it
               | wouldn't hurt to have a mechanism to optionally drop the
               | old cruft from the language. Otherwise everything stacks
               | on top in the name of backwards compatibility, but at
               | this pace, how will C++36 look like?
        
               | mackeye wrote:
               | a member of the c++ committee (herb sutter) is writing an
               | compiler for an alternative c++ syntax [0], to c++, with
               | the intent to restrict some semantics of the language for
               | less UB, surprises, etc. i think less implementation-
               | defined behavior is incredibly important; rvo vs
               | std::move, dynamic function call optimization, i wish i
               | didn't have to search asm to check for...
               | 
               | [0]: https://github.com/hsutter/cppfront
        
               | pjmlp wrote:
               | While I share the sentiment, compare C#14 with C# 1.0,
               | Java 25 with Java 1.0, Python 3.14 with Python 1.0.
               | 
               | While C++ might be worse, when you have 300+ doing
               | proposals every three years, others aren't safer from
               | similar churn, even if on smaller volume, and trying to
               | keep backwards compatibility going.
               | 
               | And we all know what happened in Python.
               | 
               | Also Rust editions contrary to what many think, only
               | cover a specific set of language evolution scenarios, it
               | isn't anything goes, nor there is support for binary
               | libraries.
               | 
               | As for a better C++, contrary to C, where it is business
               | as usual, there are actually people trying to sort things
               | out on WG21, even if isn't as well as we would like to.
               | 
               | "Making C++ Safe, Healthy, and Efficient - CppCon 2025"
               | 
               | https://youtu.be/p52mNWsh-qs?si=AEs1dtr6_iknzATv
        
               | gpm wrote:
               | > nor there is support for binary libraries.
               | 
               | This is a weird call-out because it's both completely
               | incorrect and completely irrelevant to the larger point.
               | 
               | Rust absolutely supports binary libraries. The only way
               | to use a rust library with the current rust compiler is
               | to first compile it to a binary format and then link to
               | it.
               | 
               | More so than C++ where header files (and thus generics
               | via templates) are textual.
               | 
               | Cargo, the most common build system for rust, insists on
               | compiling every library itself (with narrow exceptions -
               | that include for instance the precompiled standard
               | library that is used by just about everyone). That's just
               | a design choice of cargo, not the language.
        
               | pjmlp wrote:
               | Rust does, Rust editions do not have a story for them.
        
               | gpm wrote:
               | The story is that it must not matter which edition a
               | library was compiled with - it's the boundary layer at
               | which different editions interoperate with eachother.
        
           | mccr8 wrote:
           | Rust did exist in some form in 2011. Source: I ate lunch with
           | part of the Rust team in 2011.
        
             | pjmlp wrote:
             | Some form, meaning not market relevant.
        
         | secondcoming wrote:
         | What's the problem? It makes perfect sense to me that a const
         | object cannot be moved from, since it violates the constness.
         | Since constness goes hand in hand with thread safety you really
         | don't want that violation.
        
           | spot5010 wrote:
           | Maybe a compiler error that a const object cannot be "moved"?
           | 
           | That would force the programmer to remove the std::move,
           | making it clear that its a copy.
        
             | ziml77 wrote:
             | It's weird that they made a mistake of allowing this after
             | having so many years to learn from their mistake about
             | copies already being non-obvious (by that I mean that
             | references and copies look identical at the call sites)
        
             | fluoridation wrote:
             | There are cases where you would not want to reject such
             | code, though. For example, if std::move() is called inside
             | a template function where the type in some instantiations
             | resolves to const T, and the intent is indeed for the value
             | to be copied. If move may in some cases cause a compiler
             | error, then you would need to write specializations that
             | don't call it.
        
               | spot5010 wrote:
               | I didn't think of that, but you are right. At some point
               | I thought I understood templates r-value references work
               | but now I've forgotten.
        
             | lang4d wrote:
             | clang-tidy has a check for this case
        
           | j1elo wrote:
           | To be honest I agree that it makes sense, at least if we put
           | our hats of puritanism on the conceptual and semantical way
           | of seeing it.
           | 
           | But having std::move silently fall back to a copy constructor
           | is not a good solution.
        
       | QuadmasterXLII wrote:
       | C++ is the high rocky mountain pass between the fertile great
       | plains of C and the weird but ultimately survivable California of
       | Rust.
        
       | jmyeet wrote:
       | You read things like this and, first, you're reminded of Sideshow
       | Bob [1] and it puts Rust concepts in context, namely:
       | 
       | 1. Move semantics are to handle ownership. Ownership is a first-
       | class concept in Rust. This is why;
       | 
       | 2. C++ smart pointers (eg std::unique_ptr<>) are likewise to
       | handle ownership and incur a runtime cost where in Rust they are
       | handled by the compiler with no runtime cost. Yes you can "cheat"
       | (eg std::unique_ptr::get) and people do (they have to) but this
       | is a worse (IMHO) version than the much-maligned Rust unsafe
       | blocks;
       | 
       | 3. Not only do all features have a complexity cost but that curve
       | is exponential because of the complexity of interactions, in this
       | case move semantics and exceptions. At this point C++'s feature
       | set combined with legacy code support is not just an albatross
       | around its neck, it's an elephant seal; and
       | 
       | 4. There's a 278 page book on _C++ initialization_ [2].
       | 
       | My point here is that there are so many footguns here combined
       | with the features of modern processors that writing correct code
       | remains a Herculean (even Sisyphean) task.
       | 
       | But here's the worst part: IME all of this complexity tends to
       | attract a certain kind of engineer who falls in love with their
       | own cleverness who creates code using obscure features that
       | nobody else can understand all the true implications (and likely
       | they don't either).
       | 
       | Rust is complex because what you're doing is complex. Rust isn't
       | a panacea. It solves a certain class of problems well and that
       | class is really important (ie memory safety). We will be dealing
       | with C++ buffer overflow CVEs until the heat death of the
       | Universe. But one thing I appreciate about languages like Go is
       | how _simple_ they are.
       | 
       | I honestly think C++ is unsalvageable given its legacy.
       | 
       | [1]: https://www.youtube.com/watch?v=2WZLJpMOxS4
       | 
       | [2]: https://leanpub.com/cppinitbook
        
         | usefulcat wrote:
         | > C++ smart pointers (eg std::unique_ptr<>) are likewise to
         | handle ownership and incur a runtime cost where in Rust they
         | are handled by the compiler with no runtime cost.
         | 
         | What additional runtime cost is incurred by the use of
         | std::unique_ptr? Either compared to Rust or compared to doing
         | manual memory management in c++?
        
           | steveklabnik wrote:
           | Not your parent, but there are two ways:
           | 
           | 1. If you use a custom deleter, then there's extra stuff to
           | store that. this isn't common, and this API isn't available
           | in Rust, so... not the best argument here.
           | 
           | 2. There's ABI requirements that cause it to be passed in
           | memory, see here for details:
           | https://stackoverflow.com/questions/58339165/why-can-a-t-
           | be-...
        
       | injidup wrote:
       | You should almost never ever be writing your own move
       | constructors. Use compiler generated defaults. It's only for very
       | rare specialist classes that you need to override compiler
       | generated defaults. Many times when you think you need to you
       | often don't.
        
       | ghm2180 wrote:
       | > Let me put this in simpler terms: std::move is like putting a
       | sign on your object "I'm done with this, you can take its stuff."
       | 
       | and later:
       | 
       | > Specifically, that 'sign' (the rvalue reference type) tells the
       | compiler to select the Move Constructor instead of the Copy
       | Constructor.
       | 
       | This is the best conceptual definition of what `std::move` is. I
       | feel that is how every book should explain these concepts in C++
       | because its not a trivial language to get into for programmers
       | who have worked with differently opiniated languages like python
       | and java.
       | 
       | If you read Effective Modern C++ right Item 23 on this, it takes
       | quite a bit to figure out what its really for.
        
         | dsnr wrote:
         | In simpler terms
         | 
         | 1. You must implement a move constructor or a move assignment
         | operator in order for std::move to do anything
         | 
         | 2. The moved object could be left in an unusable state,
         | depending on your implementation, after stealing its internal
         | resources.
        
           | bitbasher wrote:
           | I never understood move semantics until I learned Rust.
           | Everything is move by default and the compiler makes sure you
           | never leave things in an unusable state.
           | 
           | This was a difficult mental hurdle to get over with Rust, but
           | once you do, move semantics make a lot more sense.
           | 
           | edit: When I said everything is move by default, I mean
           | everything that isn't "Copy", such as integers, floats, etc.
        
             | Conscat wrote:
             | What Rust loses with that decision is the ability to
             | program the "semantics" in move semantics. Rust has no
             | distinction between hypothetical place constructor and
             | value constructor.
        
               | anematode wrote:
               | A loss of functionality, but arguably a good thing, e.g.
               | moving will never throw an exception/panic so you don't
               | need an equivalent to is_nothrow_move_constructible
        
           | grogers wrote:
           | > You must implement a move constructor or a move assignment
           | operator in order for std::move to do anything
           | 
           | Bit of a nitpick, but there are sometimes other functions
           | with overloads for rvalue references to move the contents out
           | - think something like std::optional's `value() &&`. And you
           | don't necessarily need to implement those move
           | constructor/assignment functions yourself, typically the
           | compiler generated functions are what you want (i.e. the rule
           | of 5 _or 0_ )
        
           | jjmarr wrote:
           | > The moved object could be left in an unusable state,
           | depending on your implementation, after stealing its internal
           | resources.
           | 
           | The "proper" semantics are that it leaves the object in a
           | valid but unspecified state. So, invariants still hold, you
           | can call functions on it, or assign to it.
        
             | masklinn wrote:
             | > you can call functions on it
             | 
             | Only functions with no preconditions, unless the type makes
             | more guarantees as to the moved-from state.
        
               | jjmarr wrote:
               | The guarantees is that a moved-from state is in an
               | otherwise valid state.
               | 
               | So, you can do things like check if a moved from
               | std::vector is empty (often the case in practice), then
               | start appending elements to it.
        
         | QuercusMax wrote:
         | Modern C++ is hard to get into for people who learned C++ in
         | the 90s and then worked in other languages for a decade or two.
        
         | yunnpp wrote:
         | I thought "move doesn't move" was a fairly common C++ mantra at
         | this point.
        
           | locknitpicker wrote:
           | > I thought "move doesn't move" was a fairly common C++
           | mantra at this point.
           | 
           | It is. The fact that std::move is just a cast and that move
           | constructors are expected to transfer resources are basic
           | intro to C++ topics, covered in intro to constructors.
        
             | LexiMax wrote:
             | It's far too late to put the genie back in the bottle, but
             | I am morbidly curious as to why the standards committee
             | didn't choose an approach that made moves destructive.
        
               | jandrewrogers wrote:
               | It solves some rare edge cases where the destruction of
               | the moved-from object must be deferred -- the memory is
               | still live even if the object is semantically dead. Non-
               | destructive moves separate those concerns.
               | 
               | There is a related concept of "relocatable" objects in
               | C++ where the move is semantically destructive but the
               | destructor is never called for the moved-from object.
               | 
               | C++ tries to accommodate a lot of rare cases that you
               | really only see in low-level systems code. There are many
               | features in C++ that seem fairly useless to most people
               | (e.g. std::launder) but are indispensable when you come
               | across the specific problem they were intended to solve.
        
         | qbane wrote:
         | I read Effective Modern C++ years ago and was confused exactly
         | like what you describe.
        
       | ohnoesjmr wrote:
       | Do I really need care about this? I really hoped that I can just
       | not bother wrapping things in std::move and let the compiler
       | figure it out?
       | 
       | I.e. if I have
       | 
       | ``` std::string a = "hi"; std::string b = "world"; return {a, b};
       | // std::pair ``` I always assumed the compiler figures out that
       | it can move these things?
       | 
       | If not, why not? My ide tells me I should move, surely the
       | compiler has more context to figure that out?
        
         | brooke2k wrote:
         | I think there's a consequence difference between the IDE being
         | sure enough that a std::move is warranted to issue a lint,
         | versus the compiler being 100% provably certain that inserting
         | a move won't cause any issues.
        
           | ohnoesjmr wrote:
           | Sure, but by the sound of the article, the compiler won't do
           | the right thing?
           | 
           | Effectively, I'm a c++ novice, should I ever sprinkle move
           | (under the constraints of the article)? Or will the compiler
           | figure it out correctly for me and I can write my code
           | without caring about this.
        
       ___________________________________________________________________
       (page generated 2026-01-11 23:01 UTC)