This is my learning notes from the course - Mastering C++ Standard Library Features.
Smart Pointers Guidelines
- How to write robust, safe, and efficient code that uses smart pointers
- How to return/pass smart pointers from/to functions
- The role of raw pointers in Modern C++
- How to choose between raw/unique/shared pointers
- Data structure and argument passing code examples
No Allocation is Better than Allocation
- Making use of the stack and value semantic types should be preferred to dynamic allocation
- Objects on the stack are easier to reason about and more *”predictable”*
- Dynamic memory usage can have a significant cost: allocations are not free, they can reduce cache locality, and the compiler is often note able to aggressively optimize
- Sometimes allocations are necessary - allocate only when you need to
- Always use smart pointers - do not use new/delete
std::unique_ptr
as Your First Choice
- If dynamic allocation is necessary,
std::unique_ptr
should be your first choice - It is a zero-cost abstraction over new/delete (except for some rare cases)
- It models unique ownership, which is simple and easy to reason about
std::shared_ptr
Should Be Used Sparingly
std::shared_ptr
should be used sparingly- It is not a zero-cost abstraction over new/delete - it can have significant overhead due to atomic reference counting operations
- Shared ownership can be harder to reason about
Always Use std::make_xxx
Functions to Create Smart Pointers
- They prevent potential memory leaks
- They make the code terser and more readable
- They do not require
new
operator to be explicitly called - They can greatly improve performance for
std::shared_ptr
Role of Raw Pointers in Modern C++
- In Modern C++, raw pointers are *”optional references”* to an object
- A raw pointer should never imply ownership
- As long as raw pointers are not managing memory, they’re fine to use
Examples
Example 1
The first simple data structure is a forward linked list. As you can see, it is a simple template class that contains a value in a unique_ptr
to another instance of the linked list called _next
. This is very simple to understand and is a zero-cost abstraction over a raw pointer. The same ideas apply to a binary_tree
.
1 |
|
Now imagine you’re working on a 3D game or 2D application and you have expensive textures and 3D meshes that you want to share between your objects, and you also might want to destroy as soon as possible to prevent extended memory usage.
What you can do is to model them with shared_ptr
. So, in this case, the game_object
structure will have a shared_ptr
to a texture and a shared_ptr
to a 3D mesh, and they will be used for rendering. When all the game_objects
using a particular texture or mesh are destroyed, then that texture or mesh will be unloaded from memory, so that the memory usage stays low and resources can be efficiently shared between objects.
1 |
|
Finally, here’s the code showing a very simple cache data structure that contains a map that goes from a string to a weak_ptr
of T. You can think of this as a cache that models maybe least use relationship, and you can use weak_ptr
to try to access an object that may still be valid or invalid, depending on your caching strategy. If it’s valid, you can simply use the lock member function to access the data; if it’s invalid, you can detect it using the expired member function and simply evict the weak_ptr
from the cache.
1 |
|
Example 2
Observing / mutating an object: (pass object by reference)
1
2void f0(int&);
void f1(const int&);Observing / mutating a smart pointer: (pass pointer by reference)
1
2
3
4
5
6void f2(std::unique_ptr<int>&);
void f3(const std::unique_ptr<int>&);
void f4(std::shared_ptr<int>&);
void f5(const std::shared_ptr<int>&);
void f6(std::weak_ptr<int>&);
void f7(const std::weak_ptr<int>&);Transferring ownership: (pass by value)
1
void f8(std::unique_ptr<int>);
Sharing ownership: (pass by value)
1
void f9(std::shared_ptr<int>);
Observing / mutating an optional object: (pass by raw pointer)
1
2void f10(int*);
void f11(const int*);
Summary
- Learned to follow the rule of zero and the rule of five
- Understood that the smart pointers can hold generic resources thanks to custom deleters
- Used
= default
where possible to let the compiler implicitly generate copy/move operations