This post discusses unique_ptr and weak_ptr constructors, initializers and usage examples, make_unique advantages and disadvantages, unique_ptr custom deleters usage examples and important notes to pay attention to.
Last time, we visited the shared island, and that leaves us with only two more islands to visit, but lucky us, they are not that far from here. So take a deep breath (again), it’s time to sweem around the unique island. =std::move
()
Previous article: The Shared The Unique and The Weak – Initialization – Part 1
STD::UNIQUE_PTR
I really enjoyed this island on our first trip there.
Constructors
Empty / Nullptr
Same as std::shared_ptr
, we can initialize a unique_ptr
instance with an empty managed object.
std::unique_ptr<int> iu;
std::unique_ptr<int> iu2 = nullptr;
Construct From Pointer
A simple constructor which accepts a raw pointer:
std::unique_ptr<int> iu(new int(8));
Construct From Pointer & Deleter
A deleter can be passed by value, by reference, or by const
reference. The major thing to remember here, is that the deleter is being stored directly in the unique_ptr
(unlike std::shared_ptr
where it’s being stored in the control_block
, and that way will always be with a size of two pointers).
But the fact that the deleter is being stored directly inside the std::unique_ptr
creates some limitations when moving the unique_ptr
from one instance to another, as a dependency of the way it was stored.
template <typename T>
struct CustomDeleter {
CustomDeleter() = default;
CustomDeleter(const CustomDeleter&) = default;
CustomDeleter(CustomDeleter&) = default;
CustomDeleter(CustomDeleter&&) = default;
void operator()(T* p) const { delete p; };
};
{
CustomDeleter<int> deleter;
std::cout << "Example 1\n";
std::unique_ptr<int, CustomDeleter<int>> foo_unique(new int(),
CustomDeleter<int>()); std::unique_ptr<int, CustomDeleter<int>> f2 =
std::move(foo_unique); std::cout << "Example 2\n";
std::unique_ptr<int, CustomDeleter<int>&>
f3(new int(), deleter); std::unique_ptr<int, CustomDeleter<int>&> f4 =
std::move(f3); std::move(f4); std::unique_ptr<int, CustomDeleter<int>> f5 =
std::move(f4); std::cout << "Example 3\n";
std::unique_ptr<int,
const CustomDeleter<int>&> f7(new int(), deleter); std::unique_ptr<int, CustomDeleter<int>> f8 = std::move(f7); }
This type of CustomDeleter
is just one of some ways to create a custom deleter for a unique ptr
. There are some tradeoffs like readability, needs and more that can affect the specific type that you might choose for every std::unique_ptr
instance. However, on top of all those tradeoffs, there is one more thing to consider: the size of the unique_ptr
instance, due to the fact that the deleter is being stored directly inside the instance.
Function Pointer
Another pointer to store inside our std::unique_ptr
instance. In this case, we’ll get a size of sizeof(void*) * 2
. A pointer to the actual data, and a pointer to the delete
function.
std::unique_ptr<int, void(*)(int*)> my_unique(new int(), [](int*){});
std::function
This one gives us a little more overhead. The pure size of std::function
(tested on cpo.sh, 32bit os) is 24 bytes. Adding the alignment with a pointer size, we get a total size of 32 bytes.
However, we can pass it by reference to the unique_ptr
instance, and then, we’ll get a total size of two pointers (and the std::function
instance is stored outside).
std::unique_ptr<int, std::function<void(int*)>> my_unique(new int(),
[](int*){}); std::function<void(int*)> my_del = [](int*){};
std::unique_ptr<int, std::function<void(int*)>&>
my_unique2(new int(), my_del);
Lambda might be a little more trickier, if you are not familiar with the actual structure that's behind it. Therefore, I recommend reading the truth behind it.
When talking about an empty captured lambda, it’ll take by itself a single byte, because it’s the minimal size for an object. But, when it comes to storing it within std::unique_ptr
, some compilers (like llvm
) might still get a total size of 4 bytes for the unique_ptr
instance. If it seems like magic, smells like magic and can disappear like magic, does it really have to be magic? I’ll discuss further about this magical behavior in some compilers in the next article, but for now, let’s assume that there is no such a magic, and just use the expected behavior.
So, if the lambda size is 1 byte, and a pointer size is 4 bytes, with the default alignment, we’ll get a std::unique_ptr
instance with a total size of 8 bytes. The same size is used when we pass a lambda by reference.
Note: If the lambda capture is not empty, the lambda size might be different than 1, but the reference size will remain the same.
auto my_deleter = [](int*){};
std::unique_ptr<int, decltype(my_deleter)> my_unique(new int(), my_deleter);
Empty Functor
Same behavior as the lambda (which is in fact, a functor).
template <typename T>
class DeleteFunctor {
public:
void operator()(T* p) const {
delete p;
}
};
{
std::unique_ptr<int, DeleteFunctor<int>> mu(new int(), DeleteFunctor<int>());
}
Construct From Unique
As we encountered before, we can move the ownership from one uniqe_ptr
instance to another. There is no way to copy one to another, as it breaks the idea of a unique ownership. Once we moved the ownership, the origin unique_ptr
will be reset to nullptr
.
std::unique_ptr<int> mu(new int());
auto mu1 = std::move(mu);
External Unique Initializers
Similarly to std::shared_ptr
, unique_ptr
can be initialized using an external initializer std::make_unique
.
std::make_unique
Last time, we saw all of the std::make_shared
advantages. However, unique_ptr
doesn’t manage any pointer except for the managed object. So why should you use this method? Let’s take, for example, the following function:
void my_func(std::unique_ptr<int> iptr1, std::unique_ptr<int> iptr2);
my_func(std::unique_ptr<int>(new int(1)), std::unique_ptr<int>(new int(2)));
my_func(std::make_unique<int>(1), std::make_unique<int>(2));
There are three main benefits for the second call over the first call.
- The first advantage is easy to spot – less redundant initialize. When using
std::unique_ptr
constructor, we have to repeat on the concrete type two times. Once as part of the template, and again as part of the new
call. - The second advantage is the ability to avoid the
new
keyword, as part of code maintenance, and using good practice conventions. - The third advantage is the only advantage that may prevent an actual and an immediate hidden bug. But in order to understand it, and to make the bug a little more visible, there is a rule we should know.
Order Of Evaluation (until C++17)
From cppreference: “Order of evaluation of any part of any expression, including order of evaluation of function arguments is unspecified (with some exceptions listed below). The compiler can evaluate operands and other subexpressions in any order, and may choose another order when the same expression is evaluated again.“
Now, let’s take a second look on the first call example:
my_func(std::unique_ptr<int>(new int(1)), std::unique_ptr<int>(new int(2)));
Now, assuming the second expression (new int(2)
) throws exception, there will be a memory leak of new int(1)
. This issue doesn’t exist when using std::make_unique
instead.
C++17 note: Since C++17, there are several additional rules to the evaluation order, and one of them solves exactly this issue:
From cppreference: “15) In a function call, value computations and side effects of the initialization of every parameter are indeterminately sequenced with respect to value computations and side effects of any other parameter.”
That means, that the compiler must handle each parameter end-to-end before continue to the next one. The order of the handled parameters remains unspecified, but it can’t partially handle one parameter and continue to another. (Special thanks for Yehezkel Bernat for that note.)
Downsides of std::make_unique
However, this std::make_unique
function has a limitation that may not fit for any case: You can’t pass a custom deleter with it. In std::shared_ptr
, this issue has been solved using std::allocate_shared
, but there is no something like that (yet) for the unique case. There is a proposal about it, that didn’t make so far it to the standard P0211.
STD::WEAK_PTR
std::weak_ptr
is responsible for holding a pointer which is managed outside by a shared_ptr
. A weak ptr can’t be initialized without a shared_ptr
instance, or another weak_ptr
instance, unless it is initialized with nullptr
. In order to access the underlying pointer, we have to create a shared_ptr
instance out of it, and access it through it. You can read further about it on the first article in series. =.lock().
Constructors
Default Constructor
A default constructor initializes a weak_ptr
instance with a nullptr control_block
.
Initialize From Shared
weak_ptr
purpose is to be created out of a shared_ptr
instance, in order to get an access to a managed object without an ownership. The common use case is to solve the circular pointing issue.
auto is = std::make_shared<int>(); std::weak_ptr<int> iu = is;
Copy From Weak
A weak_ptr
can get a copy of another weak_ptr
instance. If the other instance owns an initialized control_block
, this call will increase the weak counter inside the shared control_block
.
auto is = std::make_shared<int>(); std::weak_ptr<int> iu = is; auto iu1 = iu;
Move From Weak
We can move one weak_ptr
instance to another, and by that, the original weak_ptr
’s control_block
will turn into nullptr
.
auto is = std::make_shared<int>(); std::weak_ptr<int> iu = is; auto iu1 = std::move(iu);
Conclusion
There is more than it usually seems in smart pointers, and there are a lot of ways to create and use them. However, there is still some hidden magic we haven’t talked about yet, and in the next article, we’ll reveal one hidden secret that some of the compilers use in order to optimize the memory space that a unique_ptr
instance takes. So stay tuned, and feel free to share (ptr) your thoughts in the comments.