C++ std::move doesn't move anything: A deep dive into Value Categories
0xghost.dev247 points by signa11 4 days ago
247 points by signa11 4 days ago
> 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.
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.
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.
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.
I sure don't miss the footguns and raw boilerplate that is having a copy constructor, move constructor, copy assignment operator, move assignment operator, and destructor, per class.
Yes, you should avoid the manual memory management that necessitates writing them. But work with a team of developers fresh out of school and next thing you know your codebase will be brimming with this kind of busywork.
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
> 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)
> 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.
> you can call functions on it
Only functions with no preconditions, unless the type makes more guarantees as to the moved-from state.
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.
I thought "move doesn't move" was a fairly common C++ mantra at this point.
> 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.
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.
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.
As someone who has actually had to launder pointers before, I would characterize gremlins like std::launder as escape hatches to dig your way out of dilemmas specific to C++ that the language was responsible for burying you under in the first place.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n13...
"There is significant desire among C++ programmers for what we call destructive move semantics [...]"
"In the end, we simply gave up on this as too much pain for not enough gain."
groan
> When dealing with class hierarchies, destructive move semantics becomes problematic. If you move the base first, then the source has a constructed derived part and a destructed base part. If you move the derived part first then the target has a constructed derived part and a not-yet-constructed base part. Neither option seems viable. Several solutions to this dilemma have been explored.
Add this to my "C++ chose the wrong kind of polymorphism to make first-class" tally.
> Add this to my "C++ chose the wrong kind of polymorphism to make first-class" tally.
Is it really the "wrong kind of polymorphism" if it isn't causing any problem and it didn't prevented rolling out features such as semantic support for move constructors?
What would you want to happen when an object that's on the stack is moved? Do you want its destructor to run, or not? If not, how exactly do you want that to no longer occur? And what do you want to happen if the stack object is moved in multiple places? How willing are you to pay a performance or UB penalty for these?
> How willing are you to pay a performance or UB penalty for these?
OP wasn't even able to frame an actual problem. So why is this blend of vacuous criticism even entertained?
I read Effective Modern C++ years ago and was confused exactly like what you describe.
> I read Effective Modern C++ years ago and was confused exactly like what you describe.
It's been a while since I read it, but if I recall correctly the book focused on special member functions and when the compiler actually stepped in for the developer, not the actual concept of move semantics. Those are different things.
Special member functions is a development experience issue, and covers aspects such as "can I get the compiler going to generate code for me". If you write code that tells the compiler it should not generate move constructors for you, often it ends up generating copy constructors. That's it.
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.
> 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!
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.
> 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.
I think you're missing a fair deal of insight into the issue.
The move semantics proposal documents this aspect in clear and unambiguous terms:
- Almost any class should be able to create a nothrow move assignment operator.
- a basic requirement is that a class must have a valid resource less state (i.e., remain in a valid state after having been moved)
- those that can't, shouldn't define move semantics.
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().
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.
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.
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.
eh, depends. for instance think about a small_vector or small_string
True, in that case it should just adopt the noexcept status of the object it holds.
Exceptions should never be enabled by default. We live in a 64bit world so allocations failing indicates some other problem.
What does processor but width have to do with the likelihood of allocation failures?
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).
> it is unlikely you will get a malloc failure
That assertion completely misses the point. The scenarios involving move constructors throwing exceptions involve objects being stuck in an inconsistent/zombie state. In fact, the whole point of a move constructors is to avoid having to allocate memory.
Exceptions can be used to indicate many kinds of errors, not just allocation failures.
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
Would you mind sharing your list?
Sure, I put quickly put them into a small markdown file. At my job we have a cmake interface target that handles these (along with some version checks and project specific stuff), but I can't publish that of course. I might put these into a cmake file at some point, not sure.
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.
If that's the issue you're facing, consider clang-query, e.g.: https://godbolt.org/z/bfG94qGan
match cxxConstructExpr(hasDeclaration(cxxConstructorDecl(isMoveConstructor(), 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.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.
If I'm not mistaken, all the pitfalls in the article have clang-tidy lints to catch
Nothing about clang-tidy is enabled by default, and getting it to run at all in realistic projects is quite a chore.
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.
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.
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.
I understand the individual rationales of C++ things but I lost the faith on the whole thing.
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.
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.
Things start to break apart when you have dependencies that adopt newer standards or use broader features. There is only so much you can do unless you would like to reimplement libraries like SKIA, doctest, Qt6 or any modern game engine. It gets worse with security and updates. At some point a library will require a newer standard otherwise you have to adopt the entire codebase and assume the entire responsibility of all security updates.
At that point you are slowly rewriting the universe. So you can also do it in Rust tbh (which provides seamless updates and integration between epochs/editions).
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
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.
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.
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.
> 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.
> 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.
C++ doesn't have zero overhead, though. The committee is unwilling to take ABI breaks and so have left performance on the table. For instance, unique_ptr<T> can't be passed in registers but T* can.
Zero overhead is a fiction the committee likes to tell themselves, but it's not true.
> 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.
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.
> 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.
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
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.
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"
Same. I’ve read all the books. Written all these things at least a few times. It’s just not doable post C++11.
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.
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.
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.
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.
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.
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.
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.
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).
Should have be called give(). But naming things correctly is hard, and the C++ committee is known to do a lot of things incorrectly
That has about the same issue: like std::move it doesn't really explain that the receiver decides.
There is no giving (or taking).
I think std::rvalue would be the least confusing name.
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
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.
Maybe std::make_movable would have been a slightly better name, but it's so much simpler to write std::move.
Split the difference with std::moveable().
Also signals it doesn't actually move, while remaining just as fast to type.
But that misses too much of the semantics. It also implies ownership transfer, even if copied.
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.
There is a difference, lots of stuff starts with make_, so lots of possible completions.
C++ is the high rocky mountain pass between the fertile great plains of C and the weird but ultimately survivable California of Rust.
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.
I found the previous discussion and article very helpful
https://news.ycombinator.com/item?id=45799157 (87 comments)
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?
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.
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.
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.
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.
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.
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.
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
May be disown would be more descriptive, but the point is that it's intended for transferring of ownership versus copying data.
> 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.
After moving a value, it needs to remain in a "valid but unspecified state".
How do you mean accessing a valid object is UB?
"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).
>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...
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.
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.
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".
My function can choose to move or not to move from an object based on io input.
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.
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`.You're right, of course - I was completely messing up in my mind what r-value reference parameters actually do, and thinking that they need to be moved to, when the whole point is that they don't, they're just a reference.
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.
The state of a moved-from value is valid but unspecified (note, not undefined). IIRC the spec says vector must be `empty()` after a move. So all implementations do the obvious thing and revert back to an empty vector.
> 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);
}
}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.
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
Thanks for the godbolt link, it really helped me understand where my mistake was. I was treating r-value references in my mind as if they are "consuming" the value, but of course, as reference types, they are not.
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.
Yes, looking at some of the code others have shared, I realized where my confusion and mistake was.
I mistakenly treated in my mind `foo(obj&& x)` as `foo(obj x)`, which would indeed move or copy statically. But with functions that take an r-value reference, you're of course absolutely right, it becomes impossible to determine statically if they will move or not.
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.