[HN Gopher] C++ move semantics from scratch (2022)
___________________________________________________________________
C++ move semantics from scratch (2022)
Author : todsacerdoti
Score : 75 points
Date : 2025-11-03 14:16 UTC (5 days ago)
(HTM) web link (cbarrete.com)
(TXT) w3m dump (cbarrete.com)
| revivalizer wrote:
| 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.
| chuckadams wrote:
| 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.
| scotty79 wrote:
| 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.
| chuckadams wrote:
| 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?
|
| https://en.cppreference.com/w/cpp/language/rule_of_three.ht
| m...
|
| The confusion kind of speaks for itself. The language is a
| construction set where the primary building block is razor
| blades.
| einpoklum wrote:
| 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.
| quuxplusone wrote:
| Ten years ago I gave a C++ conference talk titled "The
| Rule of Seven (Plus or Minus Two)" -- a reference to the
| "magic number seven" meme
|
| https://en.wikipedia.org/wiki/The_Magical_Number_Seven,_P
| lus...
|
| 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. "
| ckcheng wrote:
| I retired from wanting to write C++ when Scott Meyers retired
| from writing more Effective Modern C++.
| webdevver wrote:
| scott could not have picked a better time to retire tbh. dude
| really sold the top.
| wvenable wrote:
| 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.
| TimorousBestie wrote:
| 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.
| cyanmagenta wrote:
| 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.
| einpoklum wrote:
| > 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.
| pesfandiar wrote:
| Mathematics doesn't need to remain backward compatible.
| somethingsome wrote:
| 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.
| criemen wrote:
| 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.
| htfy96 wrote:
| 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.
| einpoklum wrote:
| 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/
| kccqzy wrote:
| 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.
| criemen wrote:
| 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.
| hgomersall wrote:
| 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.
| hyghjiyhu wrote:
| Moving by default would be too much of a footgun without a
| borrow checker imo.
| cjfd wrote:
| 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.
| Maxatar wrote:
| Why would you copy a whole list to modify a single element?
| vlovich123 wrote:
| 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.
| kccqzy wrote:
| In C++ regular types have the property that const is deep.
| If you have a const std::vector<int> the you can't modify
| any of the integers contained in this container. Naturally
| for flexibility reasons not all types are regular, pointers
| being the prominent exception, and things like
| std::string_view being modern examples of non-regular
| types.
| vlovich123 wrote:
| I feel like you could benefit from watching Scott Meyers
| about the silliness in C++ if you feel like there's a
| consistent and logical feel to the language. A lot of
| this is c++-isms masquerading sensible ideas through
| official terms (regular and non-regular types)
| HarHarVeryFunny wrote:
| 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.
| tubs wrote:
| (More so since c++17) std::move should not be used for
| returns because this pessimises optimisations.
| w10-1 wrote:
| 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.
| HarHarVeryFunny wrote:
| I'm not sure how any notion of ownership and adoption in C
| would carry over to C++, since one of the problems with C
| is that it didn't define ownership. If you have a pointer
| value in C it means nothing about ownership.
|
| Given the limits of C, "adopting" a pointer variable would
| just mean that the foster-parent now had a copy of the
| pointer, and was considered owner of it, now responsible
| for freeing it too presumably. It's basically a move,
| except the source can now mess things up by still using the
| adopted value after the adoption.
| anonnon wrote:
| > defaulted to deep copy semantics
|
| It defaulted to pass-by-value, with _shallow_ copy semantics,
| as opposed to pass by reference.
| HarHarVeryFunny wrote:
| 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.
| Maxatar wrote:
| You seem to misunderstand the meaning of deep versus
| shallow copy. This distinction has to do with how
| references/pointers get copied.
|
| In C++ the compiler generated copy constructors are
| shallow, not deep, meaning that if you want a copy to
| reconstruct the object being pointed to, you can not use
| the default copy constructor but need to supply your own
| explicit copy constructor to do so.
| HarHarVeryFunny wrote:
| No - you are confusing C and C++.
|
| In C a structure passed by value will just be shallow-
| copied.
|
| In C++ a structure/class passed by value will by copied
| using whatever copy constructor is defined by the
| structure type. If there is no explicitly defined copy
| constructor, then a default copy constructor will be used
| that does member-wise copy using the member's copy
| constructors.
|
| So, unless you have chosen to shoot yourself in the foot
| by defining a class who's copy constructor doesn't do a
| deep copy, then C++ pass-by-value will indeed do a deep
| copy.
|
| The case of a structure with a pointer member (e.g. maybe
| you defined your own string class) is certainly one where
| you would need to define an appropriate copy constructor,
| and not doing so would indeed be shooting yourself in the
| foot.
| Maxatar wrote:
| >So, unless you have chosen to shoot yourself in the foot
| by defining a class who's copy constructor doesn't do a
| deep copy, then C++ pass-by-value will indeed do a deep
| copy.
|
| You either are misspeaking, deeply confused, or quite
| possibly both.
| HarHarVeryFunny wrote:
| Do you agree that C++ pass-by-value is using copy
| constructors?
|
| Do you agree that the copy constrictors of the C++
| standard library (STL) all do deep copy?
|
| Do you agree that if you write your own class with a raw
| pointer member, you would need to write a copy
| constructor?
|
| So, what exactly are you disagreeing about ?!
|
| You seem to be saying that if you wrote a C-style
| structure (no constructors) with a pointer member, then
| C++ would behave like C, which is true, but irrelevant.
| We're talking about C++ vs C, but since C++ is a superset
| of C, then yes you can still screw yourself C-style if
| you choose to.
| jstimpfle wrote:
| "Deep copy" is quite hand-wavy. A useful program is not a
| collection of trees that you can just copy like that.
| Instead, typical program's data structures tend to have
| lots of references that let you go from anywhere to just
| about anywhere else. That's why "deep copy" is just a rough
| idea that may work for simple container types but not much
| else.
| mlmonkey wrote:
| 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!)
| wocram wrote:
| && 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.
| TinkersW wrote:
| 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.
| mlmonkey wrote:
| > 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" ?
| throwaway150 wrote:
| Your comments are very confusing.
|
| > 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?
| djmips wrote:
| 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.
| pizza234 wrote:
| > 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.
| einpoklum wrote:
| > _" 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.
| thw_9a83c wrote:
| > 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-...
| web3-is-a-scam wrote:
| 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.
| vardump wrote:
| 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.
| webdevver wrote:
| for me, its C++11. the absolute pinnacle of mankind.
|
| _everything_ has been going downhill since then. coincidence?
| i think not!
| medler wrote:
| 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
| spacechild1 wrote:
| Yes! Just to list a few personal highlights:
|
| C++14: - generalized lambda capture -
| generic lambdas
|
| C++17: - structured bindings - init
| statement for if - class template argument deduction
| (CTAD) - std::string_view - std::filesystem
| - std::variant - std::optional -
| std::to_chars() and std::from_chars()
|
| C++20: - std::format - coroutines
| (makes ASIO code so much cleaner!) - concepts -
| std::span - bit manipulation (<bit>) -
| std::bind_front - std::numbers (math constants)
| mrlongroots wrote:
| Same, I don't understand the complaints against modern
| C++. A lambda, used for things like comparators etc, is
| much simpler than structs with operators overloaded
| defined elsewhere.
|
| My only complaint is the verbosity, things like
| `std::chrono::nanonseconds` break even simple statements
| into multiple lines, and you're tempted to just use
| uint64_t instead. And `std::thread` is fine but if you
| want to name your thread you still need to get the
| underlying handle and call `pthread_setname_np`. It's
| hard work pulling off everything C++ tries to pull off.
| spacechild1 wrote:
| > And `std::thread` is fine but if you want to name your
| thread you still need to get the underlying handle and
| call `pthread_setname_np`.
|
| Yes, but here we're getting deep into platform specifics.
| An even bigger pain point are thread priorities. Windows,
| macOS and Linux differ so fundamentally in this regard
| that it's really hard to create a meaningful abstraction.
| Certain things are better left to platform APIs.
| nuertey2025 wrote:
| ```c++
|
| // To lessen verbosity, try defining the following
| convenience aliases in a header:
|
| using SystemClock_t = std::chrono::system_clock;
|
| using SteadyClock_t = std::chrono::steady_clock;
|
| using HighClock_t = std::chrono::high_resolution_clock;
|
| using SharedDelay_t =
| std::atomic<SystemClock_t::duration>;
|
| using Minutes_t = std::chrono::minutes;
|
| using Seconds_t = std::chrono::seconds;
|
| using MilliSecs_t = std::chrono::milliseconds;
|
| using MicroSecs_t = std::chrono::microseconds;
|
| using NanoSecs_t = std::chrono::nanoseconds;
|
| using DoubleSecs_t = std::chrono::duration<double>;
|
| using FloatingMilliSecs_t = std::chrono::duration<double,
| std::milli>;
|
| using FloatingMicroSecs_t = std::chrono::duration<double,
| std::micro>;
|
| ```
| spacechild1 wrote:
| 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.
| jstimpfle wrote:
| 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.
| spacechild1 wrote:
| It's not like I use these things just for the sake of it.
| All of the things I've listed above solve real practical
| issues or make the code more readable. I don't really see
| how they would influence the overall program architecture,
| let alone in a negative way.
| paulddraper wrote:
| 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.
| nurettin wrote:
| 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.
| almostgotcaught wrote:
| hn has become literally just twitter level hottakes
| macleginn wrote:
| Irregardless of the main topic of the post, combining a struct
| definition with a constructor is additionally confusing.
| anonnon wrote:
| > int& lvalueRef = (int&)x;
|
| > int&& rvalueRef = (int&&)x;
|
| Why are they casting x here?
| DLoupe wrote:
| "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.
| krona wrote:
| 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.
___________________________________________________________________
(page generated 2025-11-08 23:02 UTC)