This is my learning notes from the course - Mastering C++ Standard Library Features.
std::unique_ptr
- Resources and “unique ownership”
- What
std::unique_ptr
andstd::make_unique
are? - How move semantics make
std::unique_ptr
possible? - 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_ptr
instancef
goes out of scope, it will automatically calldelete
for 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 thatsmart
is 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
new
call
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_ptr
that require move semantics to be implemented. - The destructor of
my_unique_ptr
conditionally releases the resources: if the held pointer isnullptr
, nothing will be released. - Copies are forbidden to prevent having multiple instances of
my_unique_ptr
that 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_ptr
is 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_ptr
on 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_ptr
models 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_unique
instead of directly instantiatingstd::unique_ptr
to avoid memory leaks and improve code readability - Understood that the
std::unique_ptr
can exist thanks to move semantics - Learned that the
std::unique_ptr
is almost always a zero-cost abstraction