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