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 (I can return large objects from a function!)
- 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.
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.
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.
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.
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.
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.
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.
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.
"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.
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.
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 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.
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.
Apologies, I was referring to a "Rule of 7", but I more or less hallucinated it, since I'd heard the old "rule of 3" then "rule of 5" had been revised again, and thought they were maybe going with prime numbers?
and alluding to the fact that besides the well-established "Rule of 5" (copy construction and assignment, move construction and assignment, and destruction) a C++ class author also needs to at least think about whether to provide: default constructor, ADL swap, equality comparison operator, and/or specialized std::hash. And maybe a few more I'm forgetting right now.
In hindsight (without rewatching it right now) I remember my thesis in that talk as erring too much on the side of "isn't this confusing? how exciting! be worried about the state of things!" These days I'd try harder to convey "it's not really that hard; yes there are a lot of customization knobs, but boring rules of thumb generally suffice; newbies shouldn't actually lose sleep over this stuff; just remember these simple guidelines."
It's important to notice that you can keep well out of trouble by sticking to the rule of _zero_, i.e. relying on the defaults for copy&move ctors, assignment operators and destructor.
So, the best advice is: Don't mess with the razor blades. And these days, C++ gives you enough in the standard library to avoid messing with them yourself. But since this is C++, you _can_ always open up the safety casing and messing with things, if you really want to.
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" ?
> For someone who's used to seeing "&" as an "address of" operator (or, a pointer)
You must be talking about "&something" which takes the "address of something" but the OP does not talk about this at all. You know this because you wrote in your other comment ...
> And if this indeed the case, why is int&& x compatible with int& y ?
So you clearly understand the OP is discussing "int&&" and "int&". Those are totally different from "&something". Even a cursory reading of the OP should tell you these are references, not the "address of something" that you're probably more familiar with.
One is rvalue reference and the other is lvalue reference and I agree that the article could have explained it better what they mean. But the OP doesn't seem to be an introductory piece. It's clearly aimed at intermediate to advanced C++ developers. What I find confusing is that you're mixing up something specific like "int&&" with "&something", which are entirely different concepts.
I mean when have you ever seen "int&" to be "address of" or "pointer"? You have only seen "&something" and "int*" and "int**" be "address of" or "pointer", haven't you?
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.
I couldn’t fathom starting a new project with whatever the current C++ is now.
You can still write things the old way, if you like.
everything has been going downhill since then. coincidence? i think not!
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: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.
There are better choices if everything is built from scratch, but changing wheels from a running car isn't easy.
See also:
* https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n13... move designs
* https://www.foonathan.net/2017/09/destructive-move/
It defaulted to pass-by-value, with shallow copy semantics, as opposed to pass by reference.
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.
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.
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 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.
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.
In particular you can most of the time define morphisms between concepts.
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.
> int&& rvalueRef = (int&&)x;
Why are they casting x here?
... 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.
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.
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.
https://en.cppreference.com/w/cpp/language/rule_of_three.htm...
The confusion kind of speaks for itself. The language is a construction set where the primary building block is razor blades.
https://en.wikipedia.org/wiki/The_Magical_Number_Seven,_Plus...
and alluding to the fact that besides the well-established "Rule of 5" (copy construction and assignment, move construction and assignment, and destruction) a C++ class author also needs to at least think about whether to provide: default constructor, ADL swap, equality comparison operator, and/or specialized std::hash. And maybe a few more I'm forgetting right now.
In hindsight (without rewatching it right now) I remember my thesis in that talk as erring too much on the side of "isn't this confusing? how exciting! be worried about the state of things!" These days I'd try harder to convey "it's not really that hard; yes there are a lot of customization knobs, but boring rules of thumb generally suffice; newbies shouldn't actually lose sleep over this stuff; just remember these simple guidelines."
So, the best advice is: Don't mess with the razor blades. And these days, C++ gives you enough in the standard library to avoid messing with them yourself. But since this is C++, you _can_ always open up the safety casing and messing with things, if you really want to.
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!)
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.
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.
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" ?
> For someone who's used to seeing "&" as an "address of" operator (or, a pointer)
You must be talking about "&something" which takes the "address of something" but the OP does not talk about this at all. You know this because you wrote in your other comment ...
> And if this indeed the case, why is int&& x compatible with int& y ?
So you clearly understand the OP is discussing "int&&" and "int&". Those are totally different from "&something". Even a cursory reading of the OP should tell you these are references, not the "address of something" that you're probably more familiar with.
One is rvalue reference and the other is lvalue reference and I agree that the article could have explained it better what they mean. But the OP doesn't seem to be an introductory piece. It's clearly aimed at intermediate to advanced C++ developers. What I find confusing is that you're mixing up something specific like "int&&" with "&something", which are entirely different concepts.
I mean when have you ever seen "int&" to be "address of" or "pointer"? You have only seen "&something" and "int*" and "int**" be "address of" or "pointer", haven't you?
So, basically, you're just trolling us about a language you avoid using. Thanks, that's very helpful.