C++ move semantics from scratch (2022)

(cbarrete.com)

80 points | by todsacerdoti 6 days ago ago

84 comments

  • criemen a day ago ago

    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.

    • kccqzy a day ago ago

      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 a day ago ago

        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 a day ago ago

        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 a day ago ago

          Moving by default would be too much of a footgun without a borrow checker imo.

          • hgomersall 14 hours ago ago

            I think using a language without a borrow checker is already a massive footgun (albeit less of one for GC/RC languages). More sensible move semantics would still be a big ergonomic improvement.

          • kccqzy 9 hours ago ago

            I think the language allowing use-after-move is honestly a smaller footgun than allowing using uninitialized memory or use-after-free. C++ already has the latter.

      • vlovich123 a day ago ago

        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 a day ago ago

          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 a day ago ago

            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)

            • kccqzy 9 hours ago ago

              Oh I certainly do not feel like there's consistency in the language. The C++ language is such that everyone picks a subset that makes sense to them and is relatively consistent.

              Regular and non-regular types are however a basic idea that transcends languages. I can write a snippet in Python too:

                  import copy
                  from some.random.module.on.pypi import foo
                  a = foo()
                  b = copy.copy(a)
                  assert a == b
              
              If that assertion fails it implies the type isn't regular.
      • cjfd a day ago ago

        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 a day ago ago

          Why would you copy a whole list to modify a single element?

          • cjfd 14 hours ago ago

            A very good question indeed. But maybe you should read the post I am responding to.....

    • htfy96 a day ago ago

      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.

    • HarHarVeryFunny a day ago ago

      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.

      • w10-1 a day ago ago

        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 a day ago ago

          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.

      • tubs a day ago ago

        (More so since c++17) std::move should not be used for returns because this pessimises optimisations.

    • anonnon a day ago ago

      > defaulted to deep copy semantics

      It defaulted to pass-by-value, with shallow copy semantics, as opposed to pass by reference.

      • HarHarVeryFunny a day ago ago

        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.

        • jayd16 21 hours ago ago

          https://en.cppreference.com/w/cpp/language/copy_constructor....

          > For non-union class types, the constructor performs full member-wise copy of the object's direct base subobjects and member subobjects, in their initialization order, using direct initialization. For each non-static data member of a reference type, the copy constructor binds the reference to the same object or function to which the source reference is bound.

          Would you call this deep or shallow? I think most would call it a shallow copy.

          • HarHarVeryFunny 9 hours ago ago

            The default copy constructor does a member-wise copy, but that member-wise copy in turn uses each member's copy constructor (explicit or default), if it has one.

            So for example, if your class has members whose type is any of the STL container types (std::list, vector, map, set,etc) then you will get a deep copy since that is how those types behave.

            The semantics of a reference as part of a class are whatever you choose to make them. Is the reference referring to something within the class, maybe initialized in the constructor, or is it referring to something external such as a global variable perhaps? It's up to you to define your constructors according to the semantics of how you are using the reference. If you don't provide a copy constructor then the default member-wise copy would indeed just make a copy of the reference, which would be the right thing if you were using the reference to point to something global, but would be a bug in your code (missing copy constructor) if you needed it to refer to something in the copied class.

            Raw pointers in classes are much the same. How are you using the pointer, and what does that mean that your copy constructor needs to do? Just like with the reference example, the default member-wise copy will only be correct if you are using the pointer to point to something not owned by the containing class, and just want the copy to point to the same thing. If the pointer is an owning reference to something allocated on the heap, then maybe you would want the copy to allocate it's own storage and copy the contents, but only you would know that.

            The semantics of smart pointers are more clear cut, which is why they are to be preferred in modern C++ code. A std::unique_ptr simply can't be copied, so you would be forced to write a copy constructor to handle it. The default member-wise copy of a std::shared_ptr will just invoke it's copy constructor resulting in the copy pointing to the same object, now with an increased reference count.

            Long story short, if you are writing modern C++, using STL container types, then a copy is a deep copy. If you are writing your own container types using pointers, or implementing your own copy constructors, then "copy" means whatever you have chosen it to mean.

        • jstimpfle a day ago ago

          "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.

          • HarHarVeryFunny a day ago ago

            I didn't choose the language. "Deep copy" is the phrase that everyone uses.

            At the end of the day C++ copy (incl. pass-by-value) does whatever the copy constructors of the types you are using does, which is going to be a combination of the "copy" semantics of the standard library's types, plus that of your own types.

            Obviously you could choose to write a copy constructor that violates what most reasonable people might expect of "copy" semantics, but this is going to be rare, and I think conceptualizing C++ copy as "deep/recursive copy" holds up well even if pointers (smart or raw) are involved.

            • jstimpfle 9 hours ago ago

              Chasing an object model may work for simple programs, but as the program gets more complex, it tends toward an exercise in futility.

              The problem with the object concept is that there are many ways to partition all the data in a program into distinct objects. But types and object models need you to decide on one way of looking at the world, and it wants you to see the world like that throughout the codebase.

              Taking copying as a specific example, there may well be more than one useful way of copying stuff. The best way to achieve what you need depends not only on functional factors but also non-functional factors (such as performance and concurrency for example). The more serious and involved a program gets, the more apparent it becomes that there is not one way to copy. And the harder and more arbitrary it becomes to single out one way of copying that should be "blessed" with a special syntax.

              It's an arbitrary choice to single out a blessed way to copy, and that is also a choice against a lot of other useful ways to copy stuff. Having one blessed way makes a few things easier, but makes everything more complicated. There is now one way to copy that gets done quite implicitly using syntax, which draws on a mountain of special C++ semantics, and there are other ways that must be done using regular function calls. The end result is something that is more complex than just always making regular freestanding functions that do the thing that you want done.

              (What copy routine will be chosen implicitly by syntax/semantics is mostly predicated on types, so you can always opt to add wrapper types to accomodate other ways of doing things. Now try wrapping the members inside the thing that you want copied, working against all the implicit semantics... that must be about the point where the boilerplate becomes unbearable, where C++ gets much more verbose and unreadable than just straightforward C).

              The downside of always using function calls is that you can't profit from all the built-in implicit semantics stuff, but as said there is always a theshold of program complexity beyond which that stops being useful because there's much more stuff that the program needs to do that cannot profit from it.

              • HarHarVeryFunny 9 hours ago ago

                I honestly don't understand the problem. A C++ object is copied using it's copy constructor, simple as that. If you are using STL container types, then there are no surprises - a copy means a deep copy.

                If you are writing a custom type with a custom copy constructor then of course you can implement whatever copy semantics you want to, but that is a strength of the language not a weakness. You can implement new types whose behavior is fully controlled by yourself.

                • jstimpfle 9 hours ago ago

                  > You can implement new types whose behavior is fully controlled by yourself.

                  Exactly, that's what I do. Now notice that there is not a good incremental way to go from this object model stuff, to a toolbox that lets you build many more routines out of reusable primitives (implemented mostly as POD structs and freestanding functions). It becomes worse if one has also made a lot of use of these silly access protectors, public/protected/private.

                  Which means that, when you start out with the C++ object model, and evolve the codebase, you are invariably going to paint yourself into a corner, and proceeding requires first undoing all the fluffy object stuff.

                  That's not some furious ranting disconnected from reality on my end. I've seen it many times in practice. Show me any large complex serious systems codebase that makes extensive use of that silly implicit stuff, where all the important function calls are basically out of your control, hidden in the "whitespace"... no, that's not the way to write a complex program. It doesn't work.

                  For balance, I concede that even large programs can use some self-contained container types, like std::vector, or even custom built ones. That's where the object model still works -- small, isolated stuff. However, as you scale, you tend to not include many isolated types. You program probably needs a good grip of memory allocation for example.

                  • HarHarVeryFunny 8 hours ago ago

                    Some of the worst code (codebases) you will see is C++ code written by people who are new to the language and think that it is the right thing to do, or just cool, to use every feature of the language.

                    C++ is a massive language, that has really grown too big, and is fraying at the edges. This is probably the fate of any old language - it either keeps growing adding more modern features, or it stops growing and becomes obsolete. It's hard to always add new features while retaining backwards compatibility, but backwards compatibility is what users demand, so the result is languages like C++ that are screaming for the feature set to be refactored and simplified...

                    Using C++ well is requires knowing when it's appropriate, or not appropriate, to use the features it provides!

              • HarHarVeryFunny 8 hours ago ago

                One more thought ...

                I think it's a mistake to associate C++ classes with object-orientated design or some program-wide high level design choice. A better way to think of them is just as custom types, and as a convenient packaging of data and and the functions (class methods) that works on the type/data.

                In fact using OO concepts such as inheritance, and god forbid multiple inheritence, in C++ is generally a bad idea unless you have a very specific reason for doing so. You shouldn't have the mindset of "I'm using C++, so my classes should be using inheritance". In general keep your class hierarchies as flat as possible, and most of the time you don't want inheritance at all - you are just creating a packaging of data + associated methods.

                Of course there is also nothing stopping you from combining classes with methods with additional global functions that work with those classes, but it would be advisable if they only did so using methods provided by the class, rather than having class data members declared public and letting things outside the class modify them.

                The point* of classes - packaging data and the methods that work on the data together - is to support easy and localized future changes. If your class data members are private and only operated on by class methods, then you can change the data members however you like and users of the class will not be affected.

                Used properly, C++ classes are an enabler for writing large complex projects that are easy to maintain. If using classes is impacting the design of your project, then it really means you are using them in the wrong way.

                Edit:

                * Well, one point. The other main point/value is the support of constructors and destructors so that your classes/structs are guaranteed to be initialized and destroyed. You don't need to remember to call an initialization function, or need to worry about code that throws exceptions and therefore has many paths where structure cleanup/destruction would be needed - the destructor will always be called when the object goes out of scope.

        • Maxatar a day ago ago

          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 a day ago ago

            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 a day ago ago

              >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 a day ago ago

                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.

                • Maxatar a day ago ago

                  >Do you agree that C++ pass-by-value is using copy constructors?

                  C++ is an incredibly complex language that it's almost never the case that you can make a categorical statement this simple and have it be true or false.

                  In C++, pass by value may or may not make use of a copy constructor depending on a set of very complex rules. For example passing an rvalue by value for a class type which defines a move constructor may use the move constructor rather than the copy constructor (emphasis on may because even that isn't guaranteed). Passing an lvalue for a class type may or may not invoke the copy constructor depending on specific language rules regarding copy elision. For fundamental types, there is no copy constructor whatsoever.

                  >Do you agree that the copy constrictors of the C++ standard library (STL) all do deep copy?

                  I absolutely do not agree with this, in fact believing so is absurd and I can't possibly fathom what would lead you to believe this. The principle the C++ standard uses for copying objects is based on Alexander Stepanov's "Elements of Programming" where he defines the concept of a regular type. A regular type does not require making a deep or a shallow copy, what it requires is that copies satisfy the following:

                  The copy must preserve equivalence (if a == b, then copy(a) == copy(b)).

                  The copy must be idempotent in meaning.

                  Copying must not have side effects on the source.

                  So as an example a std::string_view can be copied, but making a copy of it is constant O(1) in both time and space. What is required is that after performing a copy, the original source remains unchanged, operator == returns true between the source and the copy, and that making a copy is an idempotent operation.

                  You can extend this further for other collections as well, a std::vector<Foo*> does not end up making deep copies of the pointers being contained, what it does do is satisfy the conditions for being a regular type.

                  >Do you agree that if you write your own class with a raw pointer member, you would need to write a copy constructor?

                  Absolutely not. Just sticking to the existing standard library types you have things like std::span, all of the iterator types which use pointers under the hood, std::initializer_list, std::string_view was already covered.

                  To be blunt, you're expressing a very superficial understanding of a very complex topic that's part of an even more complex language.

                  In general my advice is... never assume that things in C++ are as simple and straight forward as they appear. If you're using a subset of the language that works for you, great, like really good job and continue doing so and I absolutely don't want to discourage you from using the subet of C++ that works for your use case... but don't then confuse this subset for being representative of either the language as a whole or representative of the vast use cases that C++ belongs to and recognize that the advice you give which works so well for you may actually be really bad advice for someone else working within a different subset of the language.

                  • HarHarVeryFunny 10 hours ago ago

                    > I absolutely do not agree with this, in fact believing so is absurd and I can't possibly fathom what would lead you to believe this.

                    Its not clear if you have any experience with C++ or you are just trolling.

                    Try using STL containers like std::list, vector, map, set and see how they behave. Or just read the documentation.

                    You bring up move vs copy constructors... which is irrelevant since a properly implemented move should give the same result as a copy other than the side effect of invalidating the source.

                    You bother to point out that fundamental types have no constructor. Thank you einstein.

                    > In general my advice is...

                    Worthless.

                    I was programming in C++ for over a quarter of a century, up to and including C++17, before I stopping a year ago.

                    I've written massive libraries and frameworks in C++ consisting of 10's of thousands of lines of code, and eagerly using all the latest language features as soon as a better way of doing things was supported.

                    You are just an ignorant twat.

        • anonnon a day ago ago

          I consider it a "shallow" copy because it doesn't copy pointer or reference members.

          • HarHarVeryFunny 9 hours ago ago

            How is the compiler meant to guess the semantics of your pointer or reference members?

            If they are referring to static or external variables, then presumably the default member-wise "shallow" copy is what you want.

            If your pointer is instead referring to some allocated storage, then presumably you do NOT want to just copy the pointer, but the language is not a mind reader - it doesn't know what your pointer means, and it supports copy constructors precisely so that you can implement whatever is needed. The language is giving you the tools to define how you want your pointer to be "copied"... it's your choice whether an exact "shallow" copy is appropriate, or whether you need to do something else.

            This is one of many reasons why if you are trying to write modern C++ you should use C++ alternatives to C ones wherever they exist - use STL containers rather than writing your own, use smart pointers rather than raw ones, etc.

  • web3-is-a-scam a day ago ago

    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.

    • spacechild1 a day ago ago

      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 a day ago ago

        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 a day ago ago

          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 a day ago ago

        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 a day ago ago

        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.

    • vardump a day ago ago

      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 a day ago ago

      for me, its C++11. the absolute pinnacle of mankind.

      everything has been going downhill since then. coincidence? i think not!

      • medler a day ago ago

        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 a day ago ago

          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 a day ago ago

            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 a day ago ago

              > 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 a day ago ago

              ```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>;

              ```

    • almostgotcaught a day ago ago

      hn has become literally just twitter level hottakes

  • hkkwritesgmail 17 hours ago ago

    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.

  • DLoupe a day ago ago

    "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 a day ago ago

      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.

      • DLoupe 13 hours ago ago

        I think you missed my point. The problem is not lack of guarding against programmer mistakes. It's that the compiler generates unnecessary code.

  • revivalizer a day ago ago

    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 a day ago ago

      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 a day ago ago

        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 a day ago ago

          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.htm...

          The confusion kind of speaks for itself. The language is a construction set where the primary building block is razor blades.

          • quuxplusone a day ago ago

            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,_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."

          • scotty79 8 hours ago ago

            I remeber that while learning Rust and writing fairly complex programs I had so many times when compiler was in my way, but once I finally figured out how to satisfy it, I saw that if I was writing in C++ I would just shoot myself in the foot in subtle hard to debug ways if I did what I tried to do and C++ compiler would happily let me.

          • einpoklum a day ago ago

            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.

    • ckcheng a day ago ago

      I retired from wanting to write C++ when Scott Meyers retired from writing more Effective Modern C++.

      • webdevver a day ago ago

        scott could not have picked a better time to retire tbh. dude really sold the top.

    • wvenable a day ago ago

      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 a day ago ago

    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 a day ago ago

      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.

    • pesfandiar a day ago ago

      Mathematics doesn't need to remain backward compatible.

      • somethingsome a day ago ago

        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.

    • einpoklum a day ago ago

      > 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.

  • anonnon a day ago ago

    > int& lvalueRef = (int&)x;

    > int&& rvalueRef = (int&&)x;

    Why are they casting x here?

  • macleginn a day ago ago

    Irregardless of the main topic of the post, combining a struct definition with a constructor is additionally confusing.

  • dh2022 21 hours ago ago

    Articles like these make me so glad I use C# and Python and don’t have to use C++.

  • mlmonkey a day ago ago

    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!)

    • TinkersW a day ago ago

      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.

      • pizza234 a day ago ago

        > 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.

      • mlmonkey a day ago ago

        > 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 a day ago ago

          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 a day ago ago

        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.

    • wocram a day ago ago

      && 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.

    • thw_9a83c a day ago ago

      > 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-...

    • einpoklum a day ago ago

      > "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.