C++ std::move doesn't move anything: A deep dive into Value Categories
221 points
2 days ago
| 20 comments
| 0xghost.dev
| HN
ghm2180
6 hours 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.

reply
dsnr
5 hours 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.

reply
bitbasher
5 hours 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.

reply
Conscat
4 hours ago
[-]
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.
reply
anematode
3 hours ago
[-]
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
reply
grogers
4 hours 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)

reply
jjmarr
3 hours 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.

reply
masklinn
2 hours ago
[-]
> you can call functions on it

Only functions with no preconditions, unless the type makes more guarantees as to the moved-from state.

reply
jjmarr
45 minutes ago
[-]
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.

reply
qbane
1 hour ago
[-]
I read Effective Modern C++ years ago and was confused exactly like what you describe.
reply
yunnpp
4 hours ago
[-]
I thought "move doesn't move" was a fairly common C++ mantra at this point.
reply
locknitpicker
4 hours 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.

reply
LexiMax
2 hours ago
[-]
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.
reply
jandrewrogers
1 hour ago
[-]
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.

reply
QuercusMax
4 hours 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.
reply
krona
12 hours 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!

reply
beached_whale
5 hours 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.

reply
Fiveplus
10 hours 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().
reply
HarHarVeryFunny
7 hours 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.

reply
dbcpp
8 hours 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.
reply
jcelerier
5 hours ago
[-]
eh, depends. for instance think about a small_vector or small_string
reply
dbcpp
5 hours ago
[-]
True, in that case it should just adopt the noexcept status of the object it holds.
reply
immibis
10 hours 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.

reply
phkahler
10 hours ago
[-]
Exceptions should never be enabled by default. We live in a 64bit world so allocations failing indicates some other problem.
reply
zbentley
9 hours ago
[-]
What does processor but width have to do with the likelihood of allocation failures?
reply
HarHarVeryFunny
8 hours ago
[-]
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).
reply
petcat
8 hours ago
[-]
640K ought to be enough for anybody!
reply
usefulcat
3 hours ago
[-]
Exceptions can be used to indicate many kinds of errors, not just allocation failures.
reply
juliangmp
11 hours 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
reply
teraro
9 hours ago
[-]
Would you mind sharing your list?
reply
rfc3092
6 hours 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.
reply
dataflow
5 hours 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.
reply
beached_whale
5 hours 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.
reply
grogers
4 hours ago
[-]
If I'm not mistaken, all the pitfalls in the article have clang-tidy lints to catch
reply
jeffbee
1 hour ago
[-]
Nothing about clang-tidy is enabled by default, and getting it to run at all in realistic projects is quite a chore.
reply
drob518
11 hours 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.
reply
lefty2
6 hours 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.

reply
usefulcat
3 hours 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.

reply
fenwick67
5 hours 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
reply
epx
9 hours ago
[-]
I understand the individual rationales of C++ things but I lost the faith on the whole thing.
reply
chihuahua
6 hours 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.
reply
ryandrake
1 hour ago
[-]
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.
reply
benreesman
9 hours 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.

reply
zbentley
9 hours 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.

reply
HarHarVeryFunny
8 hours ago
[-]
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.

reply
kanbankaren
5 hours ago
[-]
> 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.

reply
LexiMax
2 hours ago
[-]
> 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.

reply
usefulcat
2 hours ago
[-]
> 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.

reply
cjfd
9 hours ago
[-]
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.
reply
SJC_Hacker
6 hours ago
[-]
> 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.

reply
jesse__
4 hours ago
[-]
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

reply
drob518
9 hours 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.
reply
SJC_Hacker
6 hours ago
[-]
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"

reply
CyberDildonics
6 hours ago
[-]
What does in the large mean?
reply
groundzeros2015
10 hours 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.
reply
FpUser
6 hours 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.
reply
MORPHOICES
12 hours ago
[-]
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?

reply
porise
9 hours ago
[-]
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.

reply
nickelpro
2 hours ago
[-]
> 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.

reply
HarHarVeryFunny
7 hours ago
[-]
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.

reply
meindnoch
11 hours ago
[-]
Coming from other languages with generics, it took a while for me to internalize SFINAE when writing templated code.
reply
spacechild1
8 hours ago
[-]
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).
reply
groundzeros2015
9 hours 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.

reply
usefulcat
1 hour 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.

reply
groundzeros2015
34 minutes 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.

reply
dathinab
6 hours 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.

reply
groundzeros2015
5 hours 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.

reply
Fiveplus
10 hours 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.

reply
HarHarVeryFunny
6 hours 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).

reply
rurban
12 hours 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
reply
masklinn
12 hours ago
[-]
That has about the same issue: like std::move it doesn't really explain that the receiver decides.
reply
vouwfietsman
11 hours ago
[-]
std::offer
reply
HarHarVeryFunny
8 hours ago
[-]
std::abandon() ?!
reply
repelsteeltje
10 hours ago
[-]
∆ That's actually quite accurate. I like it!
reply
spot5010
9 hours ago
[-]
Maybe a std::take to pair with it?
reply
pseidemann
8 hours ago
[-]
There is no giving (or taking).

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

reply
usrnm
7 hours 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

reply
tialaramex
5 hours 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.

reply
jsphweid
7 hours ago
[-]
std::movable
reply
QuadmasterXLII
6 hours ago
[-]
C++ is the high rocky mountain pass between the fertile great plains of C and the weird but ultimately survivable California of Rust.
reply
fooker
13 hours ago
[-]
Maybe std::make_movable would have been a slightly better name, but it's so much simpler to write std::move.
reply
magicalhippo
11 hours ago
[-]
Split the difference with std::moveable().

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

reply
fooker
15 minutes ago
[-]
reply
bitexploder
8 hours ago
[-]
But that misses too much of the semantics. It also implies ownership transfer, even if copied.
reply
krior
11 hours 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.
reply
kaashif
8 hours ago
[-]
There is a difference, lots of stuff starts with make_, so lots of possible completions.
reply
pseidemann
11 hours ago
[-]
std::rvalue
reply
injidup
6 hours ago
[-]
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.
reply
ohnoesjmr
5 hours ago
[-]
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?

reply
brooke2k
5 hours ago
[-]
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.
reply
ohnoesjmr
2 hours ago
[-]
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.

reply
cenamus
9 hours ago
[-]
I found the previous discussion and article very helpful

https://news.ycombinator.com/item?id=45799157 (87 comments)

reply
andyjohnson0
10 hours ago
[-]
> 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++?

reply
ghosty141
9 hours ago
[-]
> 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.

reply
drysine
7 hours ago
[-]
>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.

reply
pjmlp
9 hours ago
[-]
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.

reply
zabzonk
10 hours ago
[-]
> 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.

reply
yosefk
10 hours ago
[-]
TFA explains how std::move is tricky to use and this is not a feature reserved for library writers
reply
zabzonk
10 hours ago
[-]
Of course it is not reserved for library writers - nothing is. But it is not a feature that application writers should worry about overmuch.
reply
yosefk
10 hours ago
[-]
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
reply
zabzonk
9 hours ago
[-]
> 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?

reply
dathinab
6 hours ago
[-]
> 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.

reply
zabzonk
13 hours ago
[-]
Naming things is hard.
reply
pseidemann
11 hours ago
[-]
I'm convinced naming things is equivalent to choosing the right abstraction, and caching things is creating a correct "view" from given normalized data.
reply
amelius
10 hours ago
[-]
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.

reply
jmyeet
6 hours 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

reply
usefulcat
1 hour 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++?

reply
steveklabnik
22 minutes 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-...

reply
oezi
13 hours ago
[-]
The best way to think about it is that std::move is a cast.

https://stackoverflow.com/a/42340735

reply
ahoka
11 hours ago
[-]
The article explains this.
reply
shmerl
14 hours ago
[-]
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.

reply
tsimionescu
12 hours ago
[-]
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.

reply
vlovich123
13 hours ago
[-]
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.
reply
mgaunard
13 hours ago
[-]
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.

reply
vlovich123
7 hours ago
[-]
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

reply
shmerl
13 hours ago
[-]
May be disown would be more descriptive, but the point is that it's intended for transferring of ownership versus copying data.
reply
masklinn
12 hours ago
[-]
> 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.

reply
knorker
11 hours ago
[-]
After moving a value, it needs to remain in a "valid but unspecified state".

How do you mean accessing a valid object is UB?

reply
masklinn
9 hours ago
[-]
"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).
reply
drysine
11 hours ago
[-]
>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...

reply
knorker
9 hours ago
[-]
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.

reply
drysine
8 hours ago
[-]
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.

reply
tsimionescu
12 hours ago
[-]
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".
reply
adrianN
12 hours ago
[-]
My function can choose to move or not to move from an object based on io input.
reply
tsimionescu
12 hours ago
[-]
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.

reply
ben-schaaf
9 hours ago
[-]
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`.
reply
adrianN
5 hours ago
[-]
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.
reply
masklinn
12 hours ago
[-]
> 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);
        }
    }
reply
tsimionescu
11 hours ago
[-]
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.

reply
masklinn
10 hours ago
[-]
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.
reply
charcircuit
10 hours ago
[-]
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.
reply
masklinn
9 hours ago
[-]
> 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.

reply
charcircuit
2 hours ago
[-]
>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.

reply
masklinn
54 minutes ago
[-]
> 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

reply
vlovich123
7 hours ago
[-]
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.
reply
masklinn
5 hours ago
[-]
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.

reply
knorker
9 hours ago
[-]
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

reply
tialaramex
8 hours ago
[-]
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.

reply
knorker
2 minutes ago
[-]
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.

reply
charcircuit
10 hours ago
[-]
I don't understand the downvoted here. Either the compiler emits the code to call a move constructor or it doesn't.
reply
Maxatar
10 hours ago
[-]
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.

reply
charcircuit
2 hours ago
[-]
>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.

reply
HarHarVeryFunny
8 hours ago
[-]
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.

reply
fluoridation
7 hours ago
[-]
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.
reply
HarHarVeryFunny
6 hours ago
[-]
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.

reply
fluoridation
4 hours ago
[-]
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.
reply
HarHarVeryFunny
4 hours ago
[-]
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.

reply
fluoridation
4 hours ago
[-]
>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".

reply
HarHarVeryFunny
3 hours ago
[-]
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.

reply
fluoridation
2 hours ago
[-]
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.
reply
HarHarVeryFunny
30 minutes ago
[-]
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.

reply
usefulcat
1 hour ago
[-]
std::allow_move probably would have been a more accurate name for std::move.
reply
cocoto
14 hours ago
[-]
Personally I see std::move more like removing ownership because it’s not explicit from its call where the ownership is transferred.
reply
tsimionescu
12 hours ago
[-]
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"
reply
stingraycharles
13 hours ago
[-]
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.

reply
masklinn
13 hours ago
[-]
> 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.

reply
vouwfietsman
11 hours ago
[-]
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.
reply
masklinn
5 hours ago
[-]
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.
reply
dathinab
5 hours ago
[-]
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.

reply
j1elo
9 hours ago
[-]
> [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.

reply
pjmlp
9 hours ago
[-]
There was no Rust in 2011.
reply
j1elo
8 hours ago
[-]
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)

reply
pjmlp
8 hours ago
[-]
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.

reply
j1elo
8 hours ago
[-]
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?

reply
mackeye
7 hours ago
[-]
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

reply
pjmlp
7 hours ago
[-]
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

reply
gpm
6 hours ago
[-]
> 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.

reply
pjmlp
6 hours ago
[-]
Rust does, Rust editions do not have a story for them.
reply
gpm
6 hours ago
[-]
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.
reply
mccr8
7 hours ago
[-]
Rust did exist in some form in 2011. Source: I ate lunch with part of the Rust team in 2011.
reply
pjmlp
7 hours ago
[-]
Some form, meaning not market relevant.
reply
secondcoming
9 hours ago
[-]
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.
reply
spot5010
9 hours ago
[-]
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.

reply
fluoridation
7 hours ago
[-]
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.
reply
spot5010
7 hours ago
[-]
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.
reply
ziml77
8 hours ago
[-]
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)
reply
lang4d
7 hours ago
[-]
clang-tidy has a check for this case
reply
j1elo
8 hours ago
[-]
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.

reply