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:

  • std::unique_ptr
  • std::shared_ptr
  • std::weak_ptr

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: