C++ Smart Pointers

Smart pointers are my favorite C++ feature set. With C, it’s up to the programmer to keep pointer details in context – and when that happens, errors pop up like weeds. I can’t begin to count the number of times I have fixed:

  • Memory leaks
  • Freeing memory that shouldn’t be freed (e.g. pointer to statically allocated variable)
  • Freeing memory incorrectly – free() vs aligned_free(), delete vs delete[]
  • Using memory that has not yet been allocated
  • Thinking memory is still allocated after being freed because the pointer itself was not updated to NULL

C++ provides three smart pointer types that take care of these problems for us:

Table of Contents:

  1. General Benefits of Smart Pointers
  2. std::unique_ptr
  3. std::shared_ptr
  4. std::weak_ptr
  5. Converting Between the Pointers
  6. Summary
  7. Code Examples
  8. Further Reading

General Benefits of Smart Pointers

The majority of pointer issues arise because programmers must keep full pointer context in their mind at all times – forgetting a piece of the puzzle results in a bug.

With C++ smart pointers, the details are managed for you under the hood:

  • If a smart pointer goes out of scope, the appropriate deleter is called automatically. The memory is not left dangling
  • Smart pointers will automatically be set to nullptr when not initialized or when memory has been released.
  • Reference counts are be handled automatically for std::shared_ptr
  • If a special delete or free() function needs to be called, it will be specified in the pointer type and declaration, and will automatically be called on delete.

std::unique_ptr

std::unique_ptr is a construct that should be used when a piece of memory can only have one owner at at time. It is lightweight container, requiring no memory penalty to use it:

sizeof(std::unique_ptr<int>) == sizeof(int *)

When initializing std::unique_ptr, use memory that is not already allocated/owned. Using previously allocated memory can result in unexpected behavior – memory could be freed unexpectedly.

  std::unique_ptr<uint32_t> x(new uint32_t(0xff00ff00)); //good
  uint32_t t = 0x00700000;
  x = &t; //compilation error
  x.reset(&t); //works, but - here be monsters! 
               // t will be freed when it goes out of scope!

You cannot copy the std::unique_ptr object – it only has move semantics.

std::unique_ptr<uint32_t> x, y(new uint32_t(0xdeadbeef));
x.reset(new uint32_t(0xf00));
x = y; //compilation error - copy
x = std::move(y); //delete x, x = y, y = nullptr

You’re able to dereference the pointer as your normally would. You can also get access to the raw pointer managed by std::unique_ptr.

if(x) {
    //x.get() behavior is undefined if x == nullptr
    uint32_t z = *(x.get());
    //above is equivalent to
    z = *x;
}

std::unique_ptr calls delete by default – but you can specify a custom deleter. The type of the deleter you want to use is part of the pointer type, so you cannot mix and match pointers of different allocation schemes

//lambda deleter fn
auto uint32_deleter = [](uint32_t * x) {
    printf(“Deleting: %p\n”, x);
    delete x; 
};

//Deleter is part of ptr type
std::unique_ptr< uint32_t, decltype(uint32_deleter)> 
    x(new uint32_t(0xaabbccdd), uint32_deleter);

std::unique_ptr<T[]>

std::unique_ptr also has an array type (std::unique_ptr<T[]>). Depending on your usage you should also consider std::vector and std::array for managing indexable data sets.

std::make_unique

std::make_unique provides a way to construct and return a std::unique_ptr object.

Class myObject {...};
auto v1 = std::make_unique<myObject>();

std::make_unique is a C++14 feature, it doesn’t have support in C++11.

Note that std::make_unique does not allow use of a custom deleter.

std::shared_ptr

std::shared_ptr is the pointer type to be used for memory that can be owned by multiple resources at one time. std::shared_ptr maintains a reference count of pointer objects. Data managed by std::shared_ptr is only freed when there are no remaining objects pointing to the data.

std::shared_ptr is more costly than std::unique_ptr. std::shared_ptr is 2x the side of a naked pointer, as it stores a pointer to the control block and a pointer to the object. All std::shared_ptr and std::weak_ptr objects that point to the same memory will share a single control block.

In addition to the size penalty, there is also an access penalty when using std::shared_ptr: overhead from managing the control block and utilizing atomic operations when accessing the std::shared_ptr.

std::shared_ptr is initialized in the same way as std::unique_ptr:

std::shared_ptr<uint32_t> x(new uint32_t(0xdeadbeef));

Unlike std::unique_ptr, copy operations are allowed with std::shared_ptr. To make a second shared pointer to the same data, pass the std::shared_ptr object into the constructor:

std::shared_ptr<uint32_t> y(x); //copy of x
x = y; //copy of y

When initializing a `std::shared_ptr, the same rule applies: use memory that is not already allocated/owned. Using previously allocated memory can result in unexpected behavior – memory could be freed unexpectedly.

uint32_t * t = new uint32_t(0x1234abcd);
std::shared_ptr<uint32_t> a(t); //ok 
std::shared_ptr<uint32_t> b(t); //here be monsters! 
//raw ptr scariness includes "this" ptr!

Like std::unique_ptr, std::shared_ptr calls delete by default – but you can specify a custom deleter. The type of the deleter you want to use is NOT part of the pointer type for std::shared_ptr.

//lambda deleter fn
auto uint32_deleter = [](uint32_t * x) {
   printf(“Deleting: %p\n”);
    delete x; };
std::shared_ptr<uint32_t>
   x(new uint32_t(0xf00ba7), uint32_deleter);

Unlike std::unique_ptr with its array type, std::shared_ptr is only used to represent single objects.

std::allocate_shared

std::allocate_shared is used when a custom allocator is needed for a std::shared_ptr. std::allocate_shared uses the specified Allocator and passes the remaining args to the object’s constructor.

std::shared_ptr<AlignedBlob_t> ptr = std::allocate_shared(aligned_new,  arg1, arg2);

std::make_shared

Unlike std::make_unique, which is only available in C++14, std:make_shared is available in C++11.

class Thingy;
std::shared_ptr<Thingy> pThingy(new Thingy());
auto pThingy(std::make_shared<Thingy>()); 
      //Looks better, less typing, more optimized!

Note that std::make_shared does not allow use of a custom deleter.

shared_from_this

It is often desirable to make a std::shared_ptr to an object using the this pointer. As mentioned in a code example above, the danger that comes from using raw pointers to initialize std::shared_ptr applies to this as well.

C++11 works around this problem by utilizing shared_from_this. When creating a class, derive from std::enable_shared_from_this<T>. This will enable a member function called shared_from_this() which can be used to provide safe access to the this pointer by preventing duplicate control blocks for the same object.

class Thingy : public std::enable_shared_from_this<Thingy>
    {..}; //CRTP!

Thingy::shared_from_this(); //member fn (prevents duplicating ctrl blocks)
listOfThings.emplace_back(shared_from_this());

The only caveat when using shared_from_this is that it requires a std::shared_ptr to be created once before being called. It is recommended to make this std::shared_ptr in the constructor.

I will provide more detailed examples of shared_from_this in a future article.

std::weak_ptr

A std::weak_ptr is simply a std::shared_ptr that is allowed to dangle. std::weak_ptr is the same size as std::shared_ptr.

A std::weak_ptr is created by using a std::shared_ptr:

std::shared_ptr<uint32_t> x(new uint32_t(0xdeadbeef));
std::weak_ptr<uint32_t> y(x); //y is weak ptr to x

std::weak_ptr does not affect the primary reference count – data will be freed when the std::shared_ptr refererence count reaches 0:

x = nullptr; //ref cnt = 0, delete object, y now dangling

You can check whether the data pointed to by the std::weak_ptr is valid:

if(y.expired()) {...} //still a valid ptr? (here - no)

std::weak_ptr has no support for dereferencing operations. In order to access the data, you must first convert the std::weak_ptr into a std::shared_ptr object:

//for actual ptr uses, must convert to shared_ptr
std::shared_ptr<uint32_t> z = y.lock(); //null if expired
auto t(y); //similar, but triggers an exception if expired

N.B.: Even though data is freed when std::shared_ptr reference count reaches 0, the combined std::weak_ptr + std::shared_ptr reference counts must be 0 before control block is destroyed!

Converting Between The Pointers

// function prototype
std::unique_ptr<uint32_t> generate_uint(void) { ... } 

//Make a unique_ptr
std::unique_ptr<uint32_t> x(new uint32_t(0xdeadbeef));
// Convert unique_ptr to shared_ptr.
std::shared_ptr<uint32_t> y = std::move(x); //valid
y = generate_uint(); //valid
// Convert weak_ptr to shared_ptr
std::shared_ptr<uint32_t> z = y.lock(); //null if expired
auto t(y); //similar, but triggers an exception if expired

Note that there are no options for converting away from a std::shared_ptr. You’re stuck with that forever!

Summary

  • std::unique_ptr
    • sizeof(std::unique_ptr<int>) == sizeof(int *)
    • Move only!
    • calls delete by default
    • Custom deleters can be specified as part of the pointer type and pointer initialization
    • Array type is available (std::unique_ptr<T[]>), but consider std::vector or std::array
  • std::shared_ptr
    • Utilizes a control block shared between pointer objects
    • sizeof(std:shared_ptr<int> == (2 * sizeof(int *))
      • pointer to data
      • pointer to control block
    • utilizes atomic operations
    • move and copy allowed
    • calls delete by default
    • Custom deleters can be specified during pointer initialization, but are not part of the pointer type.
    • Data is freed when std::shared_ptr reference count reaches 0
  • std::weak_ptr
    • Shared pointers that can dangle
    • Same size as std::shared_ptr
    • No support for dereferencing ops
      • Must be converted to shared pointer before being dereferenced
    • Does not affect primary reference count
    • std::weak_ptr + std::shared_ptr ref counts must be 0 before control block is destroyed!

Source Code Examples

Further Reading:

Using C++ Without the Heap

Want to use C++, but worried about how much it relies on dynamic memory allocations? Our course provides a hands-on approach for learning a diverse set of patterns, tools, and techniques for writing C++ code that never uses the heap.

Learn More on the Course Page

Migrating from C to C++ Articles

2 Replies to “C++ Smart Pointers”

  1. Be careful with the use of std::smart_pointer for firmware. For example by default for something like a Cortex M4 or M33 with arm-none-eabi-gcc’s libstdc++ the internal control block is not thread-safe as internally atomics are not used as it’s configured for single threaded mode and not __GTHREAD with atomics available.

  2. I agree that is not good, but I have a hard time laying it at the feet of std::smart_pointer. It is a more fundamental problem of using a single-threaded library configuration in a multi-threaded system.

Share Your Thoughts

This site uses Akismet to reduce spam. Learn how your comment data is processed.