The article is a really good exposition of move semantics, but unfortunately many modern C++ features benefit from the pedagogical technique of “imagine this feature didn’t exist, this is why someone would want to develop it.”
I say unfortunately because this doesn’t scale. A junior programmer doesn’t have the time to process 30 years of C++’s historical development.
Mathematics (which has a much longer history and the same pedagogical problem) gets around this by consolidating foundations (Bourbaki-style enriched set theory -> category theory -> homotopy type theory, perhaps?) and by compartmentalization (a commutative algebraist usually doesn’t care about PDEs and vice versa).
I don’t see C++ taking either route, realistically.
If we want to make the math analogy, C++ seems more like the language of math (basic algebra, the notion of proofs, etc.) that everyone uses, and the compartmentalization comes when you start to apply it to specific fields (number theory, etc.). That same concept exists in the C++ community: the people who care about stuff like asynchronous networking libraries aren’t usually the people who care about SIMD math libraries, and vice versa.
I also wonder if most junior C++ programmers can shortcut a bit by just using common patterns. Articles like these I’ve always thought were geared more toward experienced programmers who are intellectually curious about the inner workings of the language.
> I don’t see C++ taking either route, realistically.
But it has been taking the "compartmentalization" route: Once a new, nicer/safer/terser idiom to express something in code emerges, people being taught the language are directed to use that, without looking into the "compartment". Some of this compartmentalization is in the language itself, some in the standard library, and some is more in the 'wild' and programming customs.
It's true, though, that if you want to write your own library, flexibly enough for public use - or otherwise cater to whatever any other programmer might throw at you - you do have to dive in rather deep into the arcane specifics.
Would it be fair to say that things are so complicated (compared to all other programming languages I've used in my professional life), because C++ pre-move semantics defaulted to deep copy semantics? It seems to be set apart in that choice from many other languages.
Deep copy is pedagogically and semantically the right choice for any mutable containers. You either make containers immutable or copies deep. Otherwise it's just an invitation for subtle bugs.
I'm not sure about that - every time I copy an object I have to think through what happens, no matter the default semantics.
C++ makes the deep copy case easier than other programming languages without top-level built-in support.
Then explain that const isn’t deep and a const container can end up mutating state? Pretending like c++ has a consistent philosophy is amusing and pretending this happened because of pedagogy is amusing. It happened because in c assignment is a copy and c++ inherited this regardless of how dumb it is as a default for containers.
It certainly makes things easier. But it also makes some things very, very, very inefficient. I want a list with millions/billions of elements. I want to regularly change one of the elements somewhere in the middle. Good luck with the copying.
Correct. As someone who maintain a 16-year-old C++ code base with new features added every day, The status quo is the best incremental improvement over deep copy semantics.
There are better choices if everything is built from scratch, but changing wheels from a running car isn't easy.
Sorry to be the nitpicker here, but - the best incremental improvement would probably have seen move-destruction instead of just moves, which keep the source object alive and force the allowance for a valid 'empty' or 'dummy' state.
C++ supports both pass-by-value and pass-by-reference parameters. Pass-by-value means making a copy (a deep copy if it's a deep type), but you could always choose to optimize by passing large parameters by reference instead, and this is common practice.
The real value of std::move is cases where you to HAVE to (effectively) make a deep copy, but still want to avoid the inefficiency. std::move supports this because moving means "stealing" the value from one variable and giving it to another. A common use case is move constructors for objects where you need to initialize the object's member variables, and can just move values passed by the caller rather than copying them.
Another important use case for std::move is returning values from functions, where the compiler will automatically use move rather than copy if available, allowing you to define functions returning large/complex return types without having to worry about the efficiency.
1990's C practice was to document whether initialization values were copied or adopted. I'm curious why the concept became "move" rather than "adopt", since move gives the parameter/data agency instead of giving agency to the consuming component.
No, pass-by-value means copying, which means whatever the copy constructor of the type implements, which for all standard types means a deep copy.
You COULD define you own type where the copy constructor did something other than deep copy (i.e. something other than copy!), just as you could choose to take a hand gun and shoot yourself in the foot.
Have you even tried modern C++? If no, how can you say that C++98 was peak?
As someone who grew up with modern C++, I can't even imagine going back to C++98 because it feels so incredibly verbose. Just compare how you iterate over a std::map and print its items in C++98 vs C++23:
// C++98:
for (std::map<std::string, int>::const_iterator it = m.begin(); it != m.end(); ++it) {
std::cout << it->first << ": " << it->second << "\n";
}
// C++23:
for (const auto& [key, value] : m) {
std::print("{}: {}\n", key, value);
}
Then there are all the features I would miss, for example:
- auto
- lambda functions and std::function
- move semantics
- std::unique_ptr and, to a lesser extent, std::shared_ptr
- variadic templates
- std::filesystem
- std::chrono
- std::thread, std::mutex, std::atomic, etc.
- a well-defined memory model for multi-threaded programs
- unordered containers
- structured bindings
- class template argument deducation
- std::format
- std::optional
- std::variant
- etc.
It's getting ever more complicated and involved. I need both of my hands to count the number of times I've tried coming back to C++ and use its object model for good effect. C++ is fine for simple things, and if you're smart you can scale it a long way (since it contains C).
But when you try to use all these funny features you're enumerating there for something serious, it will invariably end up in an overcomplicated slow compiling morass. Even just trying to make the types click for inserting something into a non-trivial templatized hashmap becomes a tedious act, and the IDE cannot help anymore either.
(Last issue I had was with catching some exception just to ignore it. Turned out catch(std::bad_alloc) doesn't work, you need write catch (std::bad_alloc&).)
I prefer writing simple C-style C++ where I write whole subsystems from scratch, and I can be very clear about the semantics from the start, design in what matters, and leave out what doesn't. Adding all the built-in object semantics baggage is too much overhead.
As someone who grew up with turbo c++ I would also miss pretty much all of these (maybe not variadic template args) but at least boost covers the library parts.
At this point, I think we need a single C++ book that captures all the best improvements since C++98 and simply skips all legacy. Just skip it as if it does not exist.
Only then will new programmers feel encouraged to look at C++ in a fresh new light. And the book remains thin.
"Another difference in Rust is that values cannot be used after a move, while they simply "should not be used, mostly" in C++"
That's one of my biggest issues with C++ today. Objects that can be moved must support a "my value was moved out" state. So every access to the object usually starts with "if (have-a-value())". It also means that the destructor is called for an object that won't be used anymore.
This is a really well written article that explains the concepts straightforwardly. I had never bothered to understand this before.
... because I gave up on C++ in 2011, after reading Scott Meyers excellent Effective C++. It made me realize I had no desire to use a language that made it so difficult to use it correctly.
I had exactly the same reaction to Effective C++, and I'd learned it back in the 90's (my first compiler didn't even support templates!). It's a wonderful book for sure, but it's a wonderfully detailed map to a minefield. The existence of guidelines like the "Rule of 7" should be raising questions as to why such a rule needs to exist in the first place.
As for this article, it really did de-mystify those strange foo&& things for me. I had no idea that they're functionally identical to references and that what C++ does with them is left up to convention. But I still felt like I had to fight against sanity loss from a horrid realization.
I don't get what's bad about rule 7. And I haven't really programmed in C++ for a decade. When you are calling derived object through a base class pointer you have a choice if you want to call the function of the base class or the function of the derived class. If you don't make it virtual it's called by pointer type, if you do, it's called by pointee type. Same goes for the destructors with only difference being that in case of virtual destructor the deatructor of a base class will be called automatically after the destructor of the derived class. So basically if you want to override methods or the destructor make your functions virtual, including the destructor.
Does it lead to problems? Surely. Should all metods be virtual by default? Probably. Should there be some keyword that indicates in derived class that a method intentionally shadows a non virtual method from the base class? Yes.
It's not a great human oriented design but it's consistent.
I like working in C++ (I don't do it professionally though) and I just never bother to read up on all the weird semantic stuff. I think the more you look into C++ the more irrational it seems but I generally just program in it like it's any other language and it's fine. It's actually even somewhat enjoyable.
Whenever I'm dealing with C++, I get tripped by the most basic of things: like for example, why use "&&" for what appears to be a pointer to a pointer? And if this indeed the case, why is int&& x compatible with int& y ?? Make up your mind: is it a pointer to a pointer, or a pointer to an int?!?
I have steadfastly avoided dealing with C++ for almost 30 years, and I am grateful that I did not have to. It seems like such a messy language with overloaded operators and symbols ( don't even get me started on Lambdas!)
If you had read like even the basic part of that article you would know that && is not a pointer to a pointer.
Anyway C++ isn't as complicated as people say, most of the so called complexity exists for a reason, so if you understand the reasoning it tends to make logical sense.
You can also mostly just stick to the core subset of the language, and only use the more obscure stuff when it is actually needed(which isn't that often, but I'm glad it exists when I need it). And move semantics is not hard to understand IMO.
> Anyway C++ isn't as complicated as people say, most of the so called complexity exists for a reason, so if you understand the reasoning it tends to make logical sense.
I think there was a comment on HN by Walter Bright, saying that at some point, C++ became too complex to be fully understood by a single person.
> You can also mostly just stick to the core subset of the language
This works well for tightly controlled codebases (e.g. Quake by Carmack), but I'm not sure how this work in general, especially when project owners change over time.
> If you had read like even the basic part of that article you would know that && is not a pointer to a pointer.
OK, let me ask this: what is "&&" ? Is it a boolean AND ? Where in that article is it explained what "&&" is, other than just handwaving, saying "it's an rvalue".
For someone who's used to seeing "&" as an "address of" operator (or, a pointer), why wouldn't "&&" mean "address of pointer" ?
Unless, you work with a large team of astronauts who ignore the coding guidelines that say to stick with a core subset but leadership doesn't reign them in and eventually you end up with a grotesque tower of babel with untold horrors that even experienced coders will be sickened by.
&& is not a pointer to a pointer, it's a temporary value. There is a huge amount of cognitive overhead in normal cpp usage because over time we have found that many of the default behaviors are wrong.
The article is a really good exposition of move semantics, but unfortunately many modern C++ features benefit from the pedagogical technique of “imagine this feature didn’t exist, this is why someone would want to develop it.”
I say unfortunately because this doesn’t scale. A junior programmer doesn’t have the time to process 30 years of C++’s historical development.
Mathematics (which has a much longer history and the same pedagogical problem) gets around this by consolidating foundations (Bourbaki-style enriched set theory -> category theory -> homotopy type theory, perhaps?) and by compartmentalization (a commutative algebraist usually doesn’t care about PDEs and vice versa).
I don’t see C++ taking either route, realistically.
If we want to make the math analogy, C++ seems more like the language of math (basic algebra, the notion of proofs, etc.) that everyone uses, and the compartmentalization comes when you start to apply it to specific fields (number theory, etc.). That same concept exists in the C++ community: the people who care about stuff like asynchronous networking libraries aren’t usually the people who care about SIMD math libraries, and vice versa.
I also wonder if most junior C++ programmers can shortcut a bit by just using common patterns. Articles like these I’ve always thought were geared more toward experienced programmers who are intellectually curious about the inner workings of the language.
> I don’t see C++ taking either route, realistically.
But it has been taking the "compartmentalization" route: Once a new, nicer/safer/terser idiom to express something in code emerges, people being taught the language are directed to use that, without looking into the "compartment". Some of this compartmentalization is in the language itself, some in the standard library, and some is more in the 'wild' and programming customs.
It's true, though, that if you want to write your own library, flexibly enough for public use - or otherwise cater to whatever any other programmer might throw at you - you do have to dive in rather deep into the arcane specifics.
Mathematics doesn't need to remain backward compatible.
IMO math is backward compatible by default unless you change the foundational axioms (rare occurrence).
In particular you can most of the time define morphisms between concepts.
Would it be fair to say that things are so complicated (compared to all other programming languages I've used in my professional life), because C++ pre-move semantics defaulted to deep copy semantics? It seems to be set apart in that choice from many other languages.
Deep copy is pedagogically and semantically the right choice for any mutable containers. You either make containers immutable or copies deep. Otherwise it's just an invitation for subtle bugs.
I'm not sure about that - every time I copy an object I have to think through what happens, no matter the default semantics. C++ makes the deep copy case easier than other programming languages without top-level built-in support.
No, it should move properly when passing by value (as in, essentially the rust move semantics). If you want a copy, that should be explicit.
3 replies →
Then explain that const isn’t deep and a const container can end up mutating state? Pretending like c++ has a consistent philosophy is amusing and pretending this happened because of pedagogy is amusing. It happened because in c assignment is a copy and c++ inherited this regardless of how dumb it is as a default for containers.
5 replies →
It certainly makes things easier. But it also makes some things very, very, very inefficient. I want a list with millions/billions of elements. I want to regularly change one of the elements somewhere in the middle. Good luck with the copying.
2 replies →
Correct. As someone who maintain a 16-year-old C++ code base with new features added every day, The status quo is the best incremental improvement over deep copy semantics.
There are better choices if everything is built from scratch, but changing wheels from a running car isn't easy.
Sorry to be the nitpicker here, but - the best incremental improvement would probably have seen move-destruction instead of just moves, which keep the source object alive and force the allowance for a valid 'empty' or 'dummy' state.
See also:
* https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n13... move designs
* https://www.foonathan.net/2017/09/destructive-move/
C++ supports both pass-by-value and pass-by-reference parameters. Pass-by-value means making a copy (a deep copy if it's a deep type), but you could always choose to optimize by passing large parameters by reference instead, and this is common practice.
The real value of std::move is cases where you to HAVE to (effectively) make a deep copy, but still want to avoid the inefficiency. std::move supports this because moving means "stealing" the value from one variable and giving it to another. A common use case is move constructors for objects where you need to initialize the object's member variables, and can just move values passed by the caller rather than copying them.
Another important use case for std::move is returning values from functions, where the compiler will automatically use move rather than copy if available, allowing you to define functions returning large/complex return types without having to worry about the efficiency.
1990's C practice was to document whether initialization values were copied or adopted. I'm curious why the concept became "move" rather than "adopt", since move gives the parameter/data agency instead of giving agency to the consuming component.
1 reply →
(More so since c++17) std::move should not be used for returns because this pessimises optimisations.
> defaulted to deep copy semantics
It defaulted to pass-by-value, with shallow copy semantics, as opposed to pass by reference.
No, pass-by-value means copying, which means whatever the copy constructor of the type implements, which for all standard types means a deep copy.
You COULD define you own type where the copy constructor did something other than deep copy (i.e. something other than copy!), just as you could choose to take a hand gun and shoot yourself in the foot.
17 replies →
I loved writing C++ back in the day, C++98 was peak.
I couldn’t fathom starting a new project with whatever the current C++ is now.
Have you even tried modern C++? If no, how can you say that C++98 was peak?
As someone who grew up with modern C++, I can't even imagine going back to C++98 because it feels so incredibly verbose. Just compare how you iterate over a std::map and print its items in C++98 vs C++23:
Then there are all the features I would miss, for example:
It's getting ever more complicated and involved. I need both of my hands to count the number of times I've tried coming back to C++ and use its object model for good effect. C++ is fine for simple things, and if you're smart you can scale it a long way (since it contains C).
But when you try to use all these funny features you're enumerating there for something serious, it will invariably end up in an overcomplicated slow compiling morass. Even just trying to make the types click for inserting something into a non-trivial templatized hashmap becomes a tedious act, and the IDE cannot help anymore either.
(Last issue I had was with catching some exception just to ignore it. Turned out catch(std::bad_alloc) doesn't work, you need write catch (std::bad_alloc&).)
I prefer writing simple C-style C++ where I write whole subsystems from scratch, and I can be very clear about the semantics from the start, design in what matters, and leave out what doesn't. Adding all the built-in object semantics baggage is too much overhead.
1 reply →
Moreover, most of the footguns were present in C++98.
Modern C++ is easier and safer than it has ever been.
The biggest slight is simply that there are high quality alternatives.
As someone who grew up with turbo c++ I would also miss pretty much all of these (maybe not variadic template args) but at least boost covers the library parts.
C++98 forced the compiler to generate a lot of useless code. Newer semantics helps to remove this overhead.
You can still write things the old way, if you like.
for me, its C++11. the absolute pinnacle of mankind.
everything has been going downhill since then. coincidence? i think not!
The new changes in C++14, 17, and 20 are really nice. It feels like the language keeps getting cleaner and easier to use well
4 replies →
hn has become literally just twitter level hottakes
At this point, I think we need a single C++ book that captures all the best improvements since C++98 and simply skips all legacy. Just skip it as if it does not exist. Only then will new programmers feel encouraged to look at C++ in a fresh new light. And the book remains thin.
"Another difference in Rust is that values cannot be used after a move, while they simply "should not be used, mostly" in C++"
That's one of my biggest issues with C++ today. Objects that can be moved must support a "my value was moved out" state. So every access to the object usually starts with "if (have-a-value())". It also means that the destructor is called for an object that won't be used anymore.
clang-tidy has a check for this. https://clang.llvm.org/extra/clang-tidy/checks/bugprone/use-...
MSVC and the Clang static analyzer have a analysis checks for this too. Not sure about GCC.
It's worth remembering though that values can be reinitialized in C++, after move.
I think you missed my point. The problem is not lack of guarding against programmer mistakes. It's that the compiler generates unnecessary code.
This is a really well written article that explains the concepts straightforwardly. I had never bothered to understand this before.
... because I gave up on C++ in 2011, after reading Scott Meyers excellent Effective C++. It made me realize I had no desire to use a language that made it so difficult to use it correctly.
I had exactly the same reaction to Effective C++, and I'd learned it back in the 90's (my first compiler didn't even support templates!). It's a wonderful book for sure, but it's a wonderfully detailed map to a minefield. The existence of guidelines like the "Rule of 7" should be raising questions as to why such a rule needs to exist in the first place.
As for this article, it really did de-mystify those strange foo&& things for me. I had no idea that they're functionally identical to references and that what C++ does with them is left up to convention. But I still felt like I had to fight against sanity loss from a horrid realization.
I don't get what's bad about rule 7. And I haven't really programmed in C++ for a decade. When you are calling derived object through a base class pointer you have a choice if you want to call the function of the base class or the function of the derived class. If you don't make it virtual it's called by pointer type, if you do, it's called by pointee type. Same goes for the destructors with only difference being that in case of virtual destructor the deatructor of a base class will be called automatically after the destructor of the derived class. So basically if you want to override methods or the destructor make your functions virtual, including the destructor.
Does it lead to problems? Surely. Should all metods be virtual by default? Probably. Should there be some keyword that indicates in derived class that a method intentionally shadows a non virtual method from the base class? Yes.
It's not a great human oriented design but it's consistent.
5 replies →
I retired from wanting to write C++ when Scott Meyers retired from writing more Effective Modern C++.
scott could not have picked a better time to retire tbh. dude really sold the top.
I like working in C++ (I don't do it professionally though) and I just never bother to read up on all the weird semantic stuff. I think the more you look into C++ the more irrational it seems but I generally just program in it like it's any other language and it's fine. It's actually even somewhat enjoyable.
> int& lvalueRef = (int&)x;
> int&& rvalueRef = (int&&)x;
Why are they casting x here?
Articles like these make me so glad I use C# and Python and don’t have to use C++.
Irregardless of the main topic of the post, combining a struct definition with a constructor is additionally confusing.
Whenever I'm dealing with C++, I get tripped by the most basic of things: like for example, why use "&&" for what appears to be a pointer to a pointer? And if this indeed the case, why is int&& x compatible with int& y ?? Make up your mind: is it a pointer to a pointer, or a pointer to an int?!?
I have steadfastly avoided dealing with C++ for almost 30 years, and I am grateful that I did not have to. It seems like such a messy language with overloaded operators and symbols ( don't even get me started on Lambdas!)
If you had read like even the basic part of that article you would know that && is not a pointer to a pointer.
Anyway C++ isn't as complicated as people say, most of the so called complexity exists for a reason, so if you understand the reasoning it tends to make logical sense.
You can also mostly just stick to the core subset of the language, and only use the more obscure stuff when it is actually needed(which isn't that often, but I'm glad it exists when I need it). And move semantics is not hard to understand IMO.
> Anyway C++ isn't as complicated as people say, most of the so called complexity exists for a reason, so if you understand the reasoning it tends to make logical sense.
I think there was a comment on HN by Walter Bright, saying that at some point, C++ became too complex to be fully understood by a single person.
> You can also mostly just stick to the core subset of the language
This works well for tightly controlled codebases (e.g. Quake by Carmack), but I'm not sure how this work in general, especially when project owners change over time.
> If you had read like even the basic part of that article you would know that && is not a pointer to a pointer.
OK, let me ask this: what is "&&" ? Is it a boolean AND ? Where in that article is it explained what "&&" is, other than just handwaving, saying "it's an rvalue".
For someone who's used to seeing "&" as an "address of" operator (or, a pointer), why wouldn't "&&" mean "address of pointer" ?
1 reply →
Unless, you work with a large team of astronauts who ignore the coding guidelines that say to stick with a core subset but leadership doesn't reign them in and eventually you end up with a grotesque tower of babel with untold horrors that even experienced coders will be sickened by.
&& is not a pointer to a pointer, it's a temporary value. There is a huge amount of cognitive overhead in normal cpp usage because over time we have found that many of the default behaviors are wrong.
> "Whenever I'm dealing with C++" ... "I have steadfastly avoided dealing with C++"
So, basically, you're just trolling us about a language you avoid using. Thanks, that's very helpful.
> Whenever I'm dealing with C++, I get tripped by...
Problem: Whenever I'm dealing with X, I get tripped by Y.
Solution A: Don't deal with X.
Solution B: Understand Y, when dealing with X.
For && meaning, this article [1] is still very useful.
[1]: https://isocpp.org/blog/2012/11/universal-references-in-c11-...