Search

15.x — Chapter 15 comprehensive review

A smart pointer class is a composition class that is designed to manage dynamically allocated memory, and ensure that memory gets deleted when the smart pointer object goes out of scope.

Copy semantics allow our classes to be copied. This is done primarily via the copy constructor and copy assignment operator.

Move semantics mean a class will transfer ownership of the object rather than making a copy. This is done primarily via the move constructor and move assignment operator.

std::auto_ptr is deprecated and should be avoided.

An r-value reference is a reference that is designed to be initialized with an r-value. An r-value reference is created using a double ampersand. It’s fine to write functions that take r-value reference parameters, but you should almost never return an r-value reference.

If we construct an object or do an assignment where the argument is an l-value, the only thing we can reasonably do is copy the l-value. We can’t assume it’s safe to alter the l-value, because it may be used again later in the program. If we have an expression “a = b”, we wouldn’t reasonably expect b to be changed in any way.

However, if we construct an object or do an assignment where the argument is an r-value, then we know that r-value is just a temporary object of some kind. Instead of copying it (which can be expensive), we can simply transfer its resources (which is cheap) to the object we’re constructing or assigning. This is safe to do because the temporary will be destroyed at the end of the expression anyway, so we know it will never be used again!

You can use the delete keyword to disable copy semantics for classes you create by deleting the copy constructor and copy assignment operator.

std::move allows you to treat an l-value as r-value. This is useful when we want to invoke move semantics instead of copy semantics on an l-value.

std::move_if_noexcept will return a movable r-value if the object has a noexcept move constructor, otherwise it will return a copyable l-value. We can use the noexcept specifier in conjunction with std::move_if_noexcept to use move semantics only when a strong exception guarantee exists (and use copy semantics otherwise).

std::unique_ptr is the smart pointer class that you should probably be using. It manages a single non-shareable resource. std::make_unique() (in C++14) should be preferred to create new std::unique_ptr. std::unique_ptr disables copy semantics.

std::shared_ptr is the smart pointer class used when you need multiple objects accessing the same resource. The resource will not be destroyed until the last std::shared_ptr managing it is destroyed. std::make_shared() should be preferred to create new std::shared_ptr. With std::shared_ptr, copy semantics should be used to create additional std::shared_ptr pointing to the same object.

std::weak_ptr is the smart pointer class used when you need one or more objects with ability to view and access a resource managed by a std::shared_ptr, but unlike std::shared_ptr, std::weak_ptr is not considered when determining whether the resource should be destroyed.

Quiz time

1) Explain when you should use the following types of pointers.

1a) std::unique_ptr

Show Solution

1b) std::shared_ptr

Show Solution

1c) std::weak_ptr

Show Solution

1d) std::auto_ptr

Show Solution

2) Explain how r-values references enable move semantics.

Show Solution

3) What’s wrong with the following code? Update the programs to be best practices compliant.

3a)

Show Solution

3b)

Show Solution


16.1 -- The Standard Library
Index
15.7 -- Circular dependency issues with std::shared_ptr, and std::weak_ptr

40 comments to 15.x — Chapter 15 comprehensive review

  • buddyteq

    Alex,

    Great website!

    I thought that one of the advantages of smart pointers is that they would insure the destructor would be called even in the case of an exception in the constructor.  That doesn't seem to be happening:

    Thank you.

    • nascardriver

      If the constructor didn't finish, then the object was never constructed and it doesn't make sense to destruct it. There'd be no way for the destructor to know which members have or haven't been created.

      • buddyteq

        Thanks for the info.

      • hausevult

        What is the difference between passing in

        to a function, and

        ? Do both of these methods not pass in copies of themselves (a std::shared_ptr<Something> pointing to a dynamically allocated Something) to function doSomething()?

        Edit: Another comment mentioned that the reasoning for this is that, due to the possibility of the compiler decided to allocate all of the Something memory blocks at once before passing them to the std::shared_ptr<Something> constructors, those Something constructors could potentially throw exceptions before the function parameters are allocated/initialized on the stack with those memory blocks. Is this description accurate?

        • nascardriver

          Until C++17, that description (And the quiz solution's reasoning) was correct. One `new Something` could complete, then the other `new Something` runs and throws an exception, and the first `Something` gets leaked.
          Since C++17, each argument is fully evaluated before any other. So when a `new Something` runs, a `shared_ptr` will be constructed from it right away. If then the other `new Something` throws, the first `Something` will be deleted by its `shared_ptr` and no leak occurs.

          Even though this code doesn't leak anymore, it has other issues.
          A `std::shared_ptr` doesn't just store a pointer to the resource, but also to a control block which is shared between all `std::shared_ptr`s for this specific resource. The control block keeps track of how many `std::shared_ptr`s there are. If you use `new`, the `std::shared_ptr` constructor has to call `new` again to allocate the control block. If you use `std::make_shared`, both the resource and control block can be allocated at once. That's faster.
          With `new`, you have to write out `std::shared_ptr`, whereas with `std::make_shared`, you can use `auto`.
          `std::make_shared` can read faster than having to piece together `new` and `std::shared_ptr`.

          There's no reason to use `new` and `delete` anymore, use smart pointers together with their `std::make_*` functions. I'm currently rewriting some lessons to reflect this.

  • AbraxasKnister

    In search of some guidelines regarding dynamic allocation. Can one say that using smart pointers and dynamic allocation is in general to be avoided in favor of classes that clean up after themselves? These can (and maybe should) use dynamic allocation. Should I use dynamic allocation as sparsely as possible directly and let the data reside in containers that do that for me?

    • nascardriver

      You almost never need `new` and `delete` thanks to smart pointers. Smart pointers are no problem at all. If you need dynamic allocation, use a smart pointer. If you do need `new` and `delete`, wrap them in a container that cleans up after itself.

      • AbraxasKnister

        Ok, if I'm using dynamic allocation "manually" I'm required to use it like

        But what is the use case for using dynamic allocation "manually"? If I need big amounts of data, I'm required (by good programming style) to have a class handling it. Either it's an STL container or it's a custom class handling some STL containers or it's a custom written container using manual (by new and delete) dynamic allocation or it's a mixup of these.

        All of these use dynamic allocation under the hood so the big data is handled in a sensible way, no need to cause the allocation explicitly.

        • nascardriver

          What do you mean by "manually"? It seems like you refer to code in functions that are not member functions. Non-member functions are rare in object-oriented programming, so I won't go into that.
          You don't need a container for every little type. If all you need is a dynamically allocated object (as a class member), use smart pointers, that's what they're there for.

        • AbraxasKnister

          > What do you mean by "manually"?

          Local variables in any function, but I'm not sure.

          > If all you need is a dynamically allocated object

          What I don't really see is *when* it is advised to make an object dynamically allocated. When it's a data member it's pretty clear I think: When it's a primitive or a container don't make it dynamically allocated as primitives are small and the containers should do the dynamic allocation themselves, if the members lifetime is not to be managed by the class also don't, else use dynamic allocation.

          But what should I do in the case of some local variable? Let's say (just for example) I have a class to store a complex function on the real line and its fourier transform with one int to store the number of samples, a char to store some flags, a double to store a timestep and two std::vector<std::complex<double>>. Every time I define one instance of this object I increase the amount of stack memory consumed by one int, one char, one double and twice everything std::vector allocates on the stack (which seems to be just one pointer to a base class). That's hardly enough to consume an unhealthy amount of stack memory. But I'd still use less if I'd use just one pointer to store it.

          • nascardriver

            > *when* it is advised to make an object dynamically allocated
            When you have a member that you can't initialize during construction of the class. This is sometimes the case when you have a member without a default constructor, but you need more data to initialize it than the data that's available in the constructor. Use a smart pointer and dynamically allocate the member when you have everything you need.

            Don't worry about memory exhaustion unless you're running on a system where resources are highly limited (eg. embedded systems) or handling of memory exhaustion is critical (eg. 24/7 running applications on servers).
            The only way you're going to fill up the stack on a consumer machine is by using recursion. I can see operations on huge amounts of data causing issues too, but it's unlikely that you're loading all that data onto the stack. If you have huge amounts of data, you're already using a container that allocates dynamically.

  • HurricaneHarry

    Greetings,

    thanks again for the tutorials, but I'd like to ask a couple of questions.

    First, did I understand all that right...
    The whole move problematic originated from the problems that arise from shallow copies of pointers for example (double deletion, dangling pointers etc.) and the fact that solving those problems creates new unintuitive solutions ("copies" that modify the original for example). To differ between those cases the definition of value categories was reworked to distill the use cases of safe movement where changing the original is of no problem because it can't be used/misused anyway. (references to values without an identity or at the end of their lifecycle).

    If I understood that correctly my main question is: Why, doing all this?
    Couldn't a function like .drainTarget(x) or .devourTarget(x) fulfill that job? Similar how .lock() , .what() and others handle stuff? Especially as the fitting type conversion for the intended use case has to be taken into account nonetheless. (std::move and such)
    Those function could even be used for operator overlaoding if needed. So instead of calling a function to change the type category to match the intended use, one could directly define the use with just the function.
    What advantages does the official solution have compared to the theoretical direct approach? What am I missing here?

    The next question refers to the use of smart pointers. Their use for lifetime management and so on is clear.
    But: Why not calling functions that modify the underlying data of the managed pointer by directly providing the managed pointer instead of the smart ptr class to a function (via .get() ) (to keep all functions agnostic of the arguments management solutions)?
    The only golden rule necessary (that I can think of right now) would be to never delete the provided pointer inside of those functions. But that shouldn't be too error prone.
    Am I missing something?

    thanks in advance

    • nascardriver

      Hello!

      > move
      With your suggestion, the caller of a function would have to rely on the function's name or documentation to understand that the function will move from the object. Neither the name nor documentation are reliable sources though. A new value category with new syntax solves this.
      Move semantics also occur without the explicit use of `std::move`, eg. when returning by value. The compiler has to decide that a move is required, this can't be done by adding new functions.

      > smart pointers
      If you write a function that takes a pointer argument but doesn't care about the object's lifetime (eg. it doesn't store the pointer anywhere), make the parameter a regular pointer. Only pass smart pointers if you need lifetime management. Unnecessarily passing smart pointers by their class type adds overhead, which should be avoided.

      • HurricaneHarry

        Thanks for the response.

        Ok, so it's as I thought with smartpointers.

        Right, forget to mention the automatism possible because of the redefinition.
        But what I meant was more the "mechanical" aspect of its realisation (when <-> how). Similar how operators offer a uniform convention but the underlying algorithms are build in the same way as "normal" functions. Looking at my comment in retrospect I indeed screwed that intention up.
        And I'm slightly surprised about the documentation comment, tbh. While it's ofc right that there is no guarantee the documentation is correct isn't it the only thing available to work with with every kind of (non open source) library?

        Btw, something that helped me quite a bit understanding the problematic was the terminology documentation from Bjarne found at: http://www.stroustrup.com/terminology.pdf
        Maybe it can help someone else.

        • nascardriver

          Sorry, I don't follow. Can you rephrase the question?

          > isn't [documentation] the only thing available to work with with every kind of (non open source) library?
          No. In an ideal scenario, the declaration that the library exposes prevents you from misusing it. For an example, let's omit a `const`:

          It's better to enforce the const-ness via language features.

          This isn't the greatest example, but it's the best I can come up with right now. The point is, no matter how well you name and document, if the language doesn't prevent mistakes, the user will make mistakes. Users don't read, they just try.

          Here's a problem that can't be solved for free with the language

          Can `p` be a `nullptr`?
          We can prevent calls like `fn(nullptr)`, eg. by using `gsl::not_null` from the guidelines support library, but to be certain, we'd have to add a run-time check. In these cases, where there's no way around it (We assume a run-time check is no option, because it slows down the program), we rely on naming and documentations. Whenever possible, prevent mistakes by making them impossible via the language.

          • HurricaneHarry

            Thanks again for your time!

            Yes, I understand and totally agree that one should use the language features to enshure things go the right way.
            That's why I said I forgot to account for the possible automatism possible because of the redefinition that is needed for the compiler to decide.
            Therefore the when <-> what/how. Because afaik that decision is not part of the algorithm carrying out the operation.
            That's what std::move (and some others) is for if the programmer wants to enforce a certain treatment. Like a hunter luring a prey with bait. The animal still decides for itself but the situation forces it to do so as the hunter had planned.
            (sry for the colorful description but it's the best I can think of right now)

            When talking about the "nothing else available except the documentation" I mean for example this:

            As std::getline always returns an istream reference the result is never numerically 0. Instead the reason is found in the basic_ios class that istream is derived from. There the operator bool overload is responsible for implicit casting the stream& to 0 after checking for all error bits and finding at least one.
            Where do I find this information if not in the documentation? (unless it's open source ofc)

            • nascardriver

              > move
              I'm still off the track. Move semantics are used automatically (More often than manually (via std::move)), eg. when you return an object by value or you pass a temporary object somewhere. So, the animal can decide to die, but the hunter can force it to die if he wants to. The animal would be the compiler, the programmer is the hunter and dying is move semantics. Man I hope I'm still making sense here. Please keep on asking if I didn't get your question.

              > documentation
              The documentation is the only quickly accessible source of information in that case. Reading the implementation or disassembling a binary should be worst case solutions.

              • HurricaneHarry

                Thanks once more for the reply.

                Regarding the docu in that case: ok, thanks for the confirmation.

                Regarding the movement:
                What I meant is actually the reversed case of what you describe, not the automatism, but the cases where the programmer "baits" the compiler into doing a specific thing.
                Like the programmer is saying:
                "Hey compiler, please move this lvalue!" and the compiler "What, lvalue? Not gonna happen, I'll copy it!"
                So the programmer uses for example std::move to transform it into an rvalue: "How about this time..?" So the compiler is like: "Hey cool a rvalue! Let's move it."
                That procedure AFAIK causes the same result as directly calling a funtion like .drainTarget, or whatever, if the underlying algorithm is the same as the one used for the move constructor.
                There is no additional magic happening behind the scenes. (afaik)
                What lead me to that question in the first place, was how pointer member access operator-> works.
                As its overloading is not really an "operator overload" (like function overloading, where one substitutes the other) but more of an "operator enhancement". As the algorithm behind operator-> looks for a redefinition of itself as long as there are places to look or it finds one returning a pointer type. Or in other words, the overloaded operator code alone wouldn't provide the needed functionality. (afaik)
                But in the end this conversation and what I found elsewhere made it quite clear I'd say.
                So, thanks!

  • jerron

    is std::forward introduced somewhere in this tutorial?

  • Dants

    for question 3b, I don't understand why make_share is a better choice? Since we are passing in two separate smart pointers, each pointing to separate objects, wouldn't make_unique be better?

  • 3a:

  • Matthias

    In 3b you wrote:

    doSomething(std::shared_ptr<Something>(new Something), std::shared_ptr<Something>(new Something));
    [...]
    If the constructor for Something throws an exception, one of the Somethings may not be deallocated properly.

    But: At the time "doSomething()" runs, both shared_ptrs are constructed and will deallocate properly at some point.
    The problem is that the compiler might choose to create both "new Something"s before storing them inside their shared_ptrs, making this essentially "tmp1 = new Something; tmp2 = new Something; tmp3 = shared_ptr(tmp1); tmp4 = shared_ptr(tmp2); doSomething(tmp3, tmp4). Then if the second "new Something" throws the first "new Something" will not be properly deallocated. Or, if the first "shared_ptr()" throws the second "new Something" will not be properly deallocated.

  • Rohit singh

    can i make a project using only c++

  • Rohit singh

    Sir can i make a project on completing this course from this site

  • Jujinko

    Hey Alex,

    I believe you are missing a ":" on the initial code for 3b) "std:shared_ptr<Something>(new Something)"

  • C++ Learner

    Hi Alex, what is the difference between make_shared and shared_ptr? they both create shared ptr, am I right?

  • Alan

    Great tutorial! I've been interested in learning some of this stuff, and the other articles I found on this topic were pretty confusing. But now I have a decent understanding of move semantics, r-value references, etc.

  • Aaron

    Hi Alex,
    Thanks for the great tutorial! Just want to know what's your plan for the next few updates(what topics will be covered/added and when(approximately)). Thank you.

    • Alex

      Right now I'm working on an index to make it easier to find specific topics. After that, I'll probably cover some miscellaneous topics. Then maybe some more stuff on containers.

      I don't have much time to write new stuff these days so I can't say when any of this is likely to show up.

  • Andrew

    I'm so glad this chapter is out! This is the best overview of move semantics and smart pointers I have read. Thank you!

  • Rom

    Why did you deleted chapters b.2-b.6? I was suppose to read it today :(

    • Alex

      Originally chapters b.2-b.6 covered C++11 material. That material has now been integrated into the primary lessons, making these chapters completely redundant. Chapter b.1 now serves as a guide to new functionality added in C++11, and has cross-references to where that material can be found within the primary lessons.

Leave a Comment

Put all code inside code tags: [code]your code here[/code]