Modern C++: Your ptr, My ptr, Our ptr

Memory management is one of the most important part of C++. Since the inception of unique_ptr, shared_ptr, weak_ptr and deprication of auto_ptr it became important that we understand how to use them. Even today there are many C++ codes running in production that needs refactoring to make use of these new features while upgrading to newer version of C++. The first C++11 migration I was involved in, we mainly did the minimum: only changed auto_ptr to unique_ptr. We did not take advantage of move semantics. I am sure there are many who did the same. But at some point we will have to slowly refactor the existing code bases and ensure that all new codes we write make use of the new features correctly. This post will help you build a better understanding of how to use these new memory management features from modern C++.

To start with lets look at what is a good memory ownership practice:

  • If a function or a class does not alter memory ownership in any way, this should be clear to every client of this function or class, as well as the implementer
  • If a function or a class takes exclusive ownership of some of the objects passed to it, this should be clear to the client (we assume that the implementer knows this already, since he/she has to write the code)
  • If a function or a class shares ownership of an object passed to it, this should be clear to the client (or anyone who reads the client code, for that matter)
  • For every object that is created, at every point that it’s used, it is clear whether this code is expected to delete it or not

In general, a good design makes it clear if a particular code owns a resource or not and a bad design needs additional knowledge that cannot be inferred from the context. As an example:

Car *ptrCar = makeCar();

Should you delete the Car once you are done with it? If you have to delete it, how do you delete it? Should you use operator delete or free()? Calling operator delete on car that was not allocated by operator new, will lead to memory corruption. Even if we use Factory pattern to create all objects, we have the same questions? Does the Factory owns the cars it created; common sense says, it should not!

Now say we use a magicTransformation() function, as shown below. Does the function deletes the ptrCar and return a new car? or is it returning the same car by making some changes?

Car *ptrCar = makeCar();
Car *ptrAnotherCar = magicTransformation(ptrCar);

If the Car is deleted we have a dangling pointer and if its not deleted but we assume it is, then we have a leak.

One may think that may be use of raw pointer (which many of us are so use to) is a bad thing in Modern C++. That’s not true. Instead we should think carefully about memory ownership, when do we really need to use a raw pointer, unique_ptr, shared_ptr, etc. Let us explore the different type of memory ownership.

Your ptr, I won’t care!

The most common type of ownership is non-ownership. Most of our codes use some objects that they don’t own. These codes just work on the objects created by someone earlier and will be deleted by someone later. So how do we say that a function is just going to use an object, but not going to delete it or try to extend it’s lifetime after its execution? We do this simply by using raw pointers as shown below.

void changeColourToRed(Car* ptrCar) {
   if (ptrCar) {
      // ...
   }
}

void mustChangeColourToRed(Car& aCar) {
   // ...
}

Both the function are changing the car object, but the first function may exit without doing anything. Also, the first function needs to do an extra check in case someone has passed a nullptr. In such case the, the function cannot do anything but exit without any action. It depends on the requirement but I personally like the second function and try to use this style where possible. Even if you have a pointer to Car object it is not a problem for the second function. It can still be called as shown:

Car *ptrCar = makeCar();
mustChangeColourToRed(*ptrCar);

Even if you pass the pointer of Car object to another object that doesn’t need to own or extend it’s lifetime, we should use raw pointers.

class CarProcessor {
   const Car* oldestCar = nullptr;
   const Car* latestCar = nullptr;
public:
   CarProcessor() = default;

   // Function to process car and find the oldest and latest car
   // from the set of cars being passed to this function
   void process(const Car* ptrCar) {
      if(ptrCar == nullptr) return;
      if(oldestCar == nullptr && latestCar == nullptr) {
         oldestCar = latestCar = ptrCar;
      else if(oldestCar->getYear() > ptrCar->getYear())
        oldestCar = ptrCar;
      else if (latestCar->getYear() < ptrCar->getYear())
        latestCar = ptrCar;
   }
   const Car* getOldestCar() { return oldestCar; }
   const Car* getLatestCar() { return latestCar; }
};

CarProcessor aCarProcessor;

aCarProcessor.process(ptrCar);

To convey that a code doesn’t own an object we should use raw pointers or references. Even in C++ 17, with all its smart pointers, there is a big place for raw pointers. In fact, majority of the pointers in a code will be raw pointers.

So to summarize, a raw pointer (T*) is not-owning and a raw reference (T&) is not-owning.

My ptr, I care!

How do we take exclusive ownership and convey to rest of the code that I and only I is responsible for its lifetime? This is where unique_ptr<> steps in. But, if you can do things without dynamic memory allocation, i.e. by using objects in stack then go for it. However, there are cases where you can’t use objects in stack. The simplest way to own a dynamically allocated object is shown below.

ToyotaCar: public Car { . . . };
std::unique_ptr<ToyotaCar> aToyotaCar = new ToyotaCar();

C++ Core Guidelines suggest that we should avoid new and delete outside resource management functions. The example we are working on should be using a Factory to create objects.

As an example, a CarFactory object should be creating and returning various type of cars. This needs a pointer. Once you get a particular type of Car object from the Factory the Car is yours. You own the Car and is responsible for its lifetime. Below is how you should be doing it:

std::unique_ptr<Car> buildCar(const int year = 0, const std::string& type = "") {
    std::unique_ptr<Car> ptrCar = nullptr;
    if (type == "Toyota")
        ptrCar = std::unique_ptr<Car>(new ToyotaCar(year));
    else
        ptrCar = std::unique_ptr<Car>(new Car(year));
    return ptrCar;
}

std::unique_ptr<Car> ptrCar = buildCar(1949);

We ensure exclusive ownership by using std::unique_ptr<Car>. The buildCar() is a factory that creates Cars dynamically and wrap it in std::unique_ptr<Car> just before shipping it out to the world. Because it is returning unique_ptr, the caller has to receive it using unique_ptr. When a unique_ptr is assigned to another unique_ptr the one on right loses the ownership. This is exactly what we want, just before the internal unique_ptr is going out of scope it passes ownership to the caller and so there is nothing to be freed when the function returns.

Another good thing about this function, the caller cannot overlook the fact that he or she needs to own the object coming out of buildCar().

You will get an error if you try to assign the pointer to a normal Car*

Car* aToyotaCar = buildCar(2000, "Toyota"); // will lead to compile-time error

error: cannot convert ‘std::unique_ptr’ to ‘Car*’ in initialization

Now how do we pass the pointers to objects or functions that just need to work with the object without the need to own the object.

cp.process(&*ptrCar);       // &*ptrCar means, address of(value at(ptrCar))
cp.process(ptrCar.get());   // the above code is same as smartPointer's get(), 
                            // which returns raw pointer without giving up ownership

Similarly we can also give the pointer to a pointer variable:

Car* anotherPtrCar = &*aCar;
anotherPtrCar = aCar.get();

Now that we own a pointer, how do we give up ownership to someone else? Below is how we do it.

std::unique_ptr<Car> aToyotaCar = buildCar(2000, "Toyota");

std::unique_ptr<Car> anotherToyotaCar = std::move(aToyotaCar);  // the right way to transfer ownership

std::unique_ptr<Car> anotherToyotaCar = aToyotaCar; // Will produce compile-time error

Note that, you will have to be explicit about it. To ensure that you are not giving up ownership unconsciously we need to use std::move. It also make the transfer of ownership more efficient. Move semantics remember? Now lets see how we can give ownership to another object.

class Owner {
private:
    std::unique_ptr<Car> myCar;
public:
    Owner() = delete;
    Owner(std::unique_ptr<Car> aCar): myCar(std::move(aCar)) {}
    ~Owner() = default;
};

Owner owner_1(std::move(anotherToyotaCar));

I guess there is not much of a surprise here, other than the double use of std::move. The first std::move was to give away ownership of anotherToyotaCar and the next was to give away ownership of aCar (constructor’s local variable) to Owner’s myCar.

C++ Core Guidelines also favor use of make_unique() to create unique pointers instead of directly calling operator new and assigning it to unique_ptr. Why? make_unique gives a more concise statement of the construction. It also ensures exception safety in complex expressions.

unique_ptr<Foo> p {new Foo{7}};    // OK: but repetitive

auto q = make_unique<Foo>(7);      // Better: no repetition of Foo

As you can see the statement with new has to use Foo multiple times, where as the other one is more compact and easy to read. I know you will say they cheated, with use of auto. But lets say we are creating a unique pointer and passing it to a function all in one statement. Now will you agree that, make unique is core compact.

 fn(fmake_unique<Foo>(7));

This is not all, there is a second reason why we should use make_unique. It ensures exception safety in complex expressions. What does it mean? In below code there is a chance of memory leak if the function_that_can_throw() throws an exception.

fn(unique_ptr<T>(new T), function_that_can_throw());

The compiler is allowed to execute the code in following order:

  1. new T
  2. function_that_can_throw()
  3. unique_ptr<T>(...)

If in step 2 an exception is raised, step 3 will never execute and have a leak. make_unique ensures that operation 1 and 3 are always one after the other. Thus, making it exception safe. Also note, exception safe doesn’t mean make_unique will not throw exception. It can throw std::bad_alloc.

Our ptr, we all care!!

Now the last part where two codes want to take responsibility of an object’s lifetime. Meaning, it’s a shared ownership where multiple parts of the code owns the object equally. This is achieved with the help of std::shared_ptr, a smart pointer that retains shared ownership of an object through a pointer. Several shared_ptr objects may own the same object. The object is destroyed and its memory deallocated when either of the following happens:

  • the last remaining shared_ptr owning the object is destroyed
  • the last remaining shared_ptr owning the object is assigned another pointer via operator= or reset()

std::shared_ptr are bit more complex, as they keep track of:

  1. number of shared_ptrs that own the managed object
  2. number of weak_ptrs that refer to the managed object

With the use of share_ptrs one will not have to worry about deletion, EVER!! A very lucrative idea! This makes all shared_ptr look like a hammer and all raw pointers look like nails. Please refrain from doing so as it comes with a cost and over-use of shared_ptr is a sign of bad design.

So what is the best place to use them? The most valid use of shared ownership are inside data structures such as lists, trees, and more. A data element may be owned by other nodes of the same data structure, by any number of iterators currently pointing to it, and, possibly, by some temporary variables inside data structure member functions that operate on the entire structure or a part of it (such as rebalancing a tree). The ownership of the entire data structure is usually clear in a well-thought-out design. But the ownership of each node, or data element, may be truly shared in the sense that any owner is equal to any other; none is privileged or primary.

Another good use case is, multi-threaded programs where a resource is shared by multiple threads. If multiple threads of execution access the same instance of shared_ptr without synchronization and any of those accesses uses a non-const member function of shared_ptr then a data race will occur; the shared_ptr overloads of atomic functions can be used to prevent the data race.

There are several disadvantages of shared ownership.

  1. If two objects with shared pointers point to each other, the entire pair remains in use indefinitely. C++ provides a solution for such problem with the use of std::weak_ptr. This circular dependency problem is real, but it happens more often in designs where shared ownership is used to conceal the larger problem of unclear resource ownership.
  2. Performance of a shared pointer is always going to be lower than that of a raw pointer.

In general I discourage use of std::shared_ptr to avoid the unclear resource ownership. This is why I am not covering this topic in details. What you don’t know can’t hurt you!!