Welcome to the next pikoTutorial!
std::unique_ptr
is one of the most common non-copyable types – types, whose constructor has been explicitly deleted in their implementation. This is beneficial in many ways. For example, such type cannot be easily provided as an input argument to the function which makes scoped resource management easier. In this tutorial we won’t however talk about this trait – we will talk about how it can be bypassed (without judging whether such workaround is good or bad).
Intro
Everything starts with the fact that if we try to execute the following code, we will get the compilation error saying that std::unique_ptr
has a deleted copy constructor.
#include <iostream>
#include <memory>
void EatPointer(std::unique_ptr<int> ptr)
{
std::cout << "value = " << *ptr << std::endl;
}
int main()
{
std::unique_ptr<int> ptr = std::make_unique<int>(12);
EatPointer(ptr);
}
This is the essence of the unique pointer and its expected behavior – only one scope may be the owner of some resource at a time and this ownership cannot be shared. Let’s look how we can bypass it.
Pass by move
Unique pointers are not copyable, but they are movable. This means that we can just move the ownership of the resource to the function’s scope by using std::move
which will convert our existing ptr
to rvalue what triggers the move constructor of the unique pointer instead of copy constructor:
#include <iostream>
#include <memory>
void EatPointer(std::unique_ptr<int> ptr)
{
std::cout << "value = " << *ptr << std::endl;
}
int main()
{
std::unique_ptr<int> ptr = std::make_unique<int>(12);
EatPointer(std::move(ptr));
}
Output:
value = 12
So it works. The consequence? Because we moved the ownership from the scope of main
function to scope of EatPointer
function, the resource is no longer in main
scope after EatPointer
function execution. For unique pointer it means, that the underlying pointer is nullified.
EatPointer(std::move(ptr));
// Be carefull! ptr is now a nullptr, so dereferencing it will cause a segmantation fault
int b = *a;
By reference
Unique pointer is just an object like any other, what means that it is stored in some place in memory. If so, then instead of passing object itself to a function, we can just pass its address. Changing function’s signature to accept ptr
by reference makes the code work without using move semantics.
#include <iostream>
#include <memory>
void EatPointer(std::unique_ptr<int> &ptr)
{
std::cout << "value = " << *ptr << std::endl;
}
int main()
{
std::unique_ptr<int> ptr = std::make_unique<int>(12);
EatPointer(ptr);
}
Consequences? We’ve just torn apart the whole idea behind using unique pointers – we declared a type which suggests exclusive ownership and then we used it in multiple scopes. Multiple, because unlike in the example with std::move
, our pointer is perfectly valid even after execution of the EatPointer
function.
By raw pointer
If passing by reference works, then passing by raw pointer will also work – in this case it will be a raw pointer to a unique pointer. The only difference in comparison to passing by reference is that in this case we must use double dereferencing operator (*
) when printing the value. This is because we first extract underlying unique pointer from a raw pointer and then from that unique pointer we extract the underlying value.
#include <iostream>
#include <memory>
void EatPointer(std::unique_ptr<int> *ptr)
{
std::cout << "value = " << **ptr << std::endl;
}
int main()
{
std::unique_ptr<int> ptr = std::make_unique<int>(12);
EatPointer(&ptr);
}
Consequences? The same as previously in case of passing by reference.
By shared pointer
Although C++ does not allow for converting shared pointer to a unique pointer (it makes sense because what would happen with all the shared ownerships if one of them limits the scope only to itself?), a conversion from unique pointer to shared pointer is valid. This means that our function can accept a shared pointer instead of unique pointer:
#include <iostream>
#include <memory>
void EatPointer(std::shared_ptr<int> ptr)
{
std::cout << "value = " << *ptr << std::endl;
}
int main()
{
std::unique_ptr<int> initial_ptr = std::make_unique<int>(12);
std::shared_ptr<int> ptr = std::move(initial_ptr);
EatPointer(ptr);
}
You may say that this is the same as the first example in which we also did std::move
on our pointer, but there is one significant difference – here, after execution of EatPointer
function, the pointer ptr
which has been passed as an argument is still usable (it is not a nullptr
). The consequence of such approach is reduced readability of the code because now one can ask “why would you ever create a unique pointer to your resource if you transfer its ownership to a shared pointer anyway?”.
By underlying raw pointer
The last one is probably the worst one, but I promised no judging at the beginning. Every smart pointer exposes get()
function which allows you to access the underlying managed resource which is a raw pointer. This pointer can be passed to a function as an argument:
#include <iostream>
#include <memory>
void EatPointer(int* ptr)
{
std::cout << "value = " << *ptr << std::endl;
}
int main()
{
std::unique_ptr<int> ptr = std::make_unique<int>(12);
EatPointer(ptr.get());
}
Consequences? In this case you are taking the resource completely out of control of your unique pointer. Moreover, if you try to use a pointer obtained by the get()
function after the std::unique_ptr
has been destroyed, you will get undefined behavior.
Note for beginners: such invalid pointer left after unique pointer destruction is called a dangling pointer.