Best Practices for C++ Smart Pointers

When to use each pointer type in C++ application design

Modern pointers

Smart pointers are part of modern C++ (from C++11) and can make expressing memory ownership in an application much clearer, but only if you take the time to understand how and where to use each of them.

Let’s explore current best practices for modern C++ smart pointer usage.

LinkedInTable of Contents

Pointer types link icon

To begin, let’s summarize modern C++ pointers (including ordinary, non-smart ones) and their general intended usage here.

std::unique_ptr - exclusive ownership link icon

Expresses exclusive ownership of an object. Can point to only one object, and the object is automatically destroyed when the pointer itself goes out of scope.

The pointer cannot be copied (but it can be moved).

Performance is the same as a raw pointer during standard operations, eg: dereferencing.

* (raw pointer) - no ownership link icon

Should be used to express that the object is not owned by the pointer at all. We’re all already familiar with this one, since it’s not actually a smart pointer. The distinction here is that from now on, we’re going to agree that we interpret its use to mean that it has no ownership of an object at all.

& (reference) - no ownership link icon

Also used to express no ownership at all. The difference is that a raw pointer is nullable.

One should prefer references whenever a null value is not a requirement of the design, since it will be valid at runtime and requires no checks for nullptr.

std::shared_ptr and std::weak_ptr - shared ownership link icon

Expresses shared ownership. An object can be pointed to by one or more shared_ptr. The object is automatically destroyed when all shared pointers that point to it, go out of scope.

Shared pointers can be copied.

Performance is less than a raw pointer, because there is additional logic surrounding the counting of references when using them.

Use shared_ptr sparingly link icon

Overuse of shared pointers may indicate bad design, and can degrade application performance. Additionally, they can introduce unique bugs, especially the circular dependency issue, where two smart pointers point at each other and the memory is locked and will never free.

Use std::weak_ptr to fix circular shared pointers link icon

The STL introduced std::weak_ptr as a counterpart to shared_ptr in order to fix the circular dependency issue when two smart pointers point at one-another. Pairing a shared pointer with a weak_ptr will allow the memory to free in this circumstance, when used properly.

\ RAII and smart pointer scope

The important thing to remember is that smart pointers leverage RAII to make managing memory more intuitive.

In general, this means that when smart pointers go out of scope (eg: reaching the end of a code block), the pointer is “smart” and does something automatically in response to the scope change.

Best practices link icon

Programmers have been converging on a set of best practices for managing memory using smart pointers in C++ since their introduction in C++11.

Like many things in the language, some of the details are still heavily debated in the community. Let’s go over some of the most important best practices to remember.

Object ownership is the key link icon

When we speak of memory management in C++, the best way to think is in terms of ownership of resources (those resources usually being objects).

The simple explanation is that much of C++’s modern memory management design relies on the concept of object ownership being at the heart of memory allocation and deallocation, by coupling object lifecycle with the RAII idiom.

Smart pointers in C++ therefore by design all rely on the concept of object ownership. eg: std::unique_ptr expresses a pointer that uniquely (exclusively) owns an object.

Using function signatures to clearly express ownership link icon

One of the benefits of consistently using smart pointers in the way we’ve described is that your function signatures will begin to document themselves in terms of their memory management expectations and design. The semantics of smart pointers can make object ownership intent of a function very obvious when used consistently.

Let’s explore some of the most common scenarios.

Functions which transfer ownership link icon

These functions generally feature std::unique_ptr as parameter, and express that unique ownership is either given or taken by the function.

\ Giving exclusive ownership (factory function)

// prepare a blank cheque
std::unique_ptr<Cheque> make_blank_cheque() {
    auto chq = std::make_unique<Cheque>();
    // ...
    return std::move(chq);
}

\ Taking exclusive ownership (consumer)

// at the end of this function, the chq object
//  is deleted since it runs out of scope
float deposit_cheque(std::unique_ptr<Cheque> chq) {
    return balance;
}
// usage - caller must explicitly consent to moving
//  the object (will not compile otherwise)
auto chq = make_blank_cheque();
deposit_cheque(std::move(chq));

Functions which do not modify ownership link icon

Functions which do work on objects (without modifying memory ownership) fall into this category. They are characterized by taking raw pointers and references as parameter.

The unspoken rule is that these functions promise not to modify the object’s lifetime (eg: delete it).

\ Non-ownership: & or * as parameter

// & or * ==> no ownership
float get_account_number(Cheque& chq);
float get_account_number(Cheque* chq);

// use with .get() on a std::unique_ptr
auto chq = std::make_unique<Cheque>();
auto num = get_account_number(chq.get());

Shared ownership functions link icon

You’ve probably guessed, these will feature std::shared_ptr in their signatures.

\ Creating a shared ownership resource

// image should be available to all, until nobody needs it
std::shared_ptr<Image> image_load(const std::string& path) {
    return std::make_shared<Image>(path);
}

Passing a shared pointer to a function will implicitly make a copy of the pointer (and increase the reference count).

\ Delegating partial ownership of a shared resource

void renderThumbnail(std::shared_ptr<Image> img) {
    // ...
}

void renderFull(std::shared_ptr<Image> img) {
    // ...
}

Beware of shared_ptr link icon

Best practices suggest to be suspicious of using shared_ptr very often, for most problems. Often, the problem can be solved with a unique_ptr exchanging ownership or providing non-owning access instead.

They are best reserved for particular scenarios, eg: in a data structure where nodes don’t have a clear single owner. For example, a tree or list where a node might be owned by one or more other nodes at runtime, depending how it is used.

\ One of the problems with shared_ptr

If we continue to think in terms of object ownership, overuse of std::shared_ptr muddies our thinking because it expresses a many-to-one relationship where many objects own another object. It often becomes hard to keep track and reason about how the owned object’s lifecycle is managed. If you encounter that, it may be an indication that a better memory ownership design exists.

Summary of best practices link icon

Let’s summarise some best practices when managing memory and object ownership in modern C++.

General link icon

References link icon

Comments