This is my learning notes from the course - Mastering C++ Standard Library Features.
std::unique_ptr
- Resources and “unique ownership”
- What
std::unique_ptrandstd::make_uniqueare? - How move semantics make
std::unique_ptrpossible? - Run-time overhead
Resources and “Unique Ownership”
- Every *”resource”* needs to be acquired and then released
- Dynamic memory, files, and threads are all examples of resources
- *”Unique ownership”*: there is only a single owner of a particular resource, who is responsible for both its acquisition and release
- Access to the resource is only possible through its owner
Here’s an example:
1 |
|
From the above example, we can see that:
- To use smart pointers, the
<memory>header must be included. std::unique_ptr<T>is a move-only class that represents unique ownership over a dynamically allocated object.- When the
unique_ptrinstancefgoes out of scope, it will automatically calldeletefor us, making sure that we do not forget to free the previously allocated memory. not_so_smart()andsmart()functions are equivalent in behavior and performance. The main difference is thatsmartis way less error-prone than the other function and way easier to understand.
As you can see, std::unique_ptr<T> requires an explicit template parameter for the owned type. It has a constructor taking a T* that takes ownership of the passed pointer. For more details, please see https://en.cppreference.com/w/cpp/memory/unique_ptr
Move Semantics
As previously mentioned, std::unique_ptr<T> is a move-only class. Why?
The idea of “copying” doesn’t make sense in the context of “unique ownership”. How can we copy an unique owner of a resource? The whole point is that the resource will be accessible only through a single owner at any point in time, that is responsible for acquiring and releasing it.
1 | void does_not_compile() |
On the other hand, the concept of “moving” is well-defined and makes sense for unique_ptr. The idea is using *”move semantics”* to represent ownership transfer from a unique_ptr to another.
1 | void ownership_transfer() |
After std::move(f) in the above example, f loses its ownership of the heap allocated foo instance. f2 becomes the only new unique owner. In this situation, when f2 goes out of scope, ~foo() will be invoked. When f goes out of scope, its destructor will not produce any side effect.
Move semantics allow us to safely and intuitively return and accept std::unique_ptr instances from/to functions.
1 | std::unique_ptr<foo> bar() |
std::make_unique
1 |
|
Consider the above code in line 14, there are three orders in which the foo() invocation could be executed:
- Order #0:
- Allocate memory for
new int{5} - Construct
unique_ptr - Invoke
bar()and throw
- Allocate memory for
- Order #1:
- Invoke
bar()and throw - Allocate memory for
new int{5} - Construct
unique_ptr
- Invoke
- Order #2:
- Allocate memory for
new int{5} - Invoke
bar()and throw - Construct
unique_ptr
- Allocate memory for
In the case of #0 and #1, everything is perfectly safe. The unique_ptr cleans up after itself if required and no memory leaks occur.
However, in the case of order #2, there is a memory leak! The unique_ptr doesn’t get a chance to complete its construction before the exception is thrown. However, the memory for the int was already allocated, so it gets leaked.
Now, consider the alternative foo() call below. By using std::make_unique prevents the issue explained above - the foo() invocation will not interleave an allocation with the call to bar().
1 | int main() |
Additionally, using make_unique:
- Makes the code terser and more readable
- Doesn’t require an explicit
newcall
How does std::unique_ptr work?
How does std::unique_ptr work? Why are move semantics necessary for its implementation?
Let’s answer those questions by creating our own own barebones my_unique_ptr.
1 |
|
From the above simple implementation, we can see that:
- The code demonstrates the concepts behind
unique_ptrthat require move semantics to be implemented. - The destructor of
my_unique_ptrconditionally releases the resources: if the held pointer isnullptr, nothing will be released. - Copies are forbidden to prevent having multiple instances of
my_unique_ptrthat refer to the same resource. - Moves make sure that the *”moved-from object”*’s held pointer is set to
nullptr, so that it loses its ownership over the resource and does not release it upon destruction.
Run-Time Overhead
unique_ptris considered a zero-cost abstraction, meaning that it will have no overhead on your application or library, except for very rare cases, which will not be covered here.- The example of
unique_ptron Compiler Explorer: http://godbolt.org/g/EdBxSu.
As you can see from the example on Compiler Explorer, the assembly outputs are exactly the same for both cases.
Summary
std:unique_ptrmodels unique ownership over a dynamically allocated object- It takes ownership of dynamically allocated memory with its constructor, and releases it in its destructor
- Ownership can be transferred to other instances by using
std::move - Used
std::make_uniqueinstead of directly instantiatingstd::unique_ptrto avoid memory leaks and improve code readability - Understood that the
std::unique_ptrcan exist thanks to move semantics - Learned that the
std::unique_ptris almost always a zero-cost abstraction