Readit News logoReadit News
ghm2180 · a month 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.

dsnr · a month ago
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 · a month ago
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.

grogers · a month ago
> 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 · a month ago
> 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.

yunnpp · a month ago
I thought "move doesn't move" was a fairly common C++ mantra at this point.
locknitpicker · a month ago
> 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.

QuercusMax · a month ago
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.
qbane · a month ago
I read Effective Modern C++ years ago and was confused exactly like what you describe.
locknitpicker · a month ago
> 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.

krona · a month ago
> 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!

beached_whale · a month ago
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.

locknitpicker · a month ago
> 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.

Fiveplus · a month ago
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().
HarHarVeryFunny · a month ago
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.

dbcpp · a month ago
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.
immibis · a month ago
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.

phkahler · a month ago
Exceptions should never be enabled by default. We live in a 64bit world so allocations failing indicates some other problem.
juliangmp · a month ago
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 · a month ago
Would you mind sharing your list?
rfc3092 · a month ago
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 · a month ago
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.

beached_whale · a month ago
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.
grogers · a month ago
If I'm not mistaken, all the pitfalls in the article have clang-tidy lints to catch
jeffbee · a month ago
Nothing about clang-tidy is enabled by default, and getting it to run at all in realistic projects is quite a chore.
drob518 · a month ago
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.
lefty2 · a month ago
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 · a month ago
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.

epx · a month ago
I understand the individual rationales of C++ things but I lost the faith on the whole thing.
chihuahua · a month ago
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.
fenwick67 · a month ago
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
FpUser · a month ago
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.
benreesman · a month ago
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 · a month ago
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.

drob518 · a month ago
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.
CyberDildonics · a month ago
What does in the large mean?
groundzeros2015 · a month ago
Same. I’ve read all the books. Written all these things at least a few times. It’s just not doable post C++11.
groundzeros2015 · a month ago
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 · a month ago
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 · a month ago
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.

Deleted Comment

usefulcat · a month ago
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 · a month ago
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.

jmyeet · a month ago
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 · a month ago
> 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 · a month ago
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-...

Dead Comment

Fiveplus · a month ago
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 · a month ago
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).

rurban · a month ago
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 · a month ago
That has about the same issue: like std::move it doesn't really explain that the receiver decides.
vouwfietsman · a month ago
std::offer
pseidemann · a month ago
There is no giving (or taking).

I think std::rvalue would be the least confusing name.

usrnm · a month ago
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 · a month ago
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 · a month ago
std::movable
fooker · a month ago
Maybe std::make_movable would have been a slightly better name, but it's so much simpler to write std::move.
magicalhippo · a month ago
Split the difference with std::moveable().

Also signals it doesn't actually move, while remaining just as fast to type.

fooker · a month ago
bitexploder · a month ago
But that misses too much of the semantics. It also implies ownership transfer, even if copied.
krior · a month ago
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 · a month ago
There is a difference, lots of stuff starts with make_, so lots of possible completions.
pseidemann · a month ago
std::rvalue