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()
vsaligned_free()
,delete
vsdelete[]
- 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:
- General Benefits of Smart Pointers
std::unique_ptr
std::shared_ptr
std::weak_ptr
- Converting Between the Pointers
- Summary
- Code Examples
- 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
orfree()
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 considerstd::vector
orstd::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
smart_ptr_aligned.cpp
usesstd::unique_ptr
std::shared_ptr
Further Reading:
- Ditch Your C-Style Pointers for Smart Pointers
- C++ Smart Pointers with Aligned Malloc/Free
- std::shared_ptr and shared_from_this
- Effective Modern C++
std::unique_ptr
std::make_unique
std::shared_ptr
std::allocate_shared
shared_from_this
std::make_shared
std::weak_ptr
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
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.
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.