Last Update:
5 ways how unique_ptr enhances resource safety in your code
Table of Contents
Modern C++ stresses the use of RAII objects to manage resources. One of the easiest ways is just to start using unique_ptr
across your code.
Let’s see how we can leverage this smart pointer type. I’ve come up with 5 (or more?) reasons where unique_ptr
shines.
Intro
One of my favourite features of modern C++ is smart pointers. They are maybe not magic bullets that solve all resource management problems. Still, they play a significant role in enforcing safety. By using them, you can avoid memory leaks, access violations errors and be sure the object clean up is done right.
While shared_ptr
and weak_ptr
are more complex, unique_ptr
seems to be a perfect replacement for owning raw pointers. Not to mention is the fact that this pointer type is mostly a compile time “wrapper” and it cost almost nothing in the runtime.
So for today, I’ve selected five… six strong points of unique_ptr
. Think where, in your project, could you immediately apply such pointers. Or maybe you have done that already? :)
Let’s start!
0. Just use the stack
I know we’re talking about memory allocations, pointers, etc, etc… but at the beginning, I’d like to make a little remark.
Sometimes using the heap is necessary, for example when your object needs to live longer than the current scope, or when you don’t know the exact size of the object (it needs to be computed in the runtime), or when your object is large.
Still, think about the stack… maybe your object can be put there? Often, I’ve seen that an object is allocated on the heap, while it could be safely placed on the stack.
The stack is faster, safer and just works :)
The default stack size is around 1MB in MSVC (platform specific), that included memory for the call stack, params, etc…. but if your object is not super large and you’re not doing any deep recursions, it should be good enough.
Also remember that containers, like vector, list, or even string will use heap anyway - they will take some space on the stack (like for small buffer optimization, or small string optimization), but they will allocate a chunk for the elements on the free store.
Let’s move to the first point now.
1. unique_ptr
replaces auto_ptr
Maybe you’re a bit insulted by this point. I know you’re not that person that still uses auto_ptr
… but let’s be honest, we all know auto_ptr
is still hiding in our projects :)
Such pointers lurk from their caves and see a chance to cause bugs in the system!
But let’s be serious this time:
With C++11 auto_ptr
got deprecated and in C++17 it’s removed from the standard. So we “got” 6 years to abandon and refactor old code.
For example, here’s what happens when compiling a code with auto_ptr
with MCSV and conformance flag set to C++17 (/std:c++latest
):
error C2039: 'auto_ptr': is not a member of 'std'
Notice that it’s an error, not a warning.
So what should you use instead?
Of course, it’s the hero of our story: unique_ptr
!
The main advantage of using modern smart pointers is the ability to express ownership transfer properly. With auto_ptr
you could easily get into troubles. Take a look here:
void doSomethig(std::auto_ptr<Test> myPtr) {
myPtr->m_value = 11;
}
void AutoPtrTest() {
std::auto_ptr<Test> myTest(new Test());
doSomethig(myTest);
myTest->m_value = 10;
}
What happens after doSomething()
returns and we want to access myTest->m_value
?
Or here’s the full (be careful!) example:
#include <memory>
#include <iostream>
class Test
{
public:
Test():m_value(0) { std::cout << "Test::Test\n"; }
~Test() { std::cout << "Test::~Test destructor\n"; }
int m_value;
};
void doSomethig(std::auto_ptr<Test> myPtr) {
myPtr->m_value = 11;
}
void AutoPtrTest() {
std::auto_ptr<Test> myTest(new Test());
doSomethig(myTest);
myTest->m_value = 10; // ??
}
int main()
{
AutoPtrTest();
}
In other words: auto_ptr
is limited - mostly by not having move semantics supported at the time it was introduced. It’s also not reference counted (as shared pointers), so its use is somewhat cumbersome.
How to refactor your code? from WG21 N4190:
auto_ptr
has been superseded byunique_ptr
. Any code usingauto_ptr
can be mechanically converted to usingunique_ptr
, withmove()
inserted wheneverauto_ptr
was being “copied”. clang-modernize’s Replace-AutoPtr Transform does exactly this.
And here’s the link to clang: clang-tidy - modernize-replace-auto-ptr — Extra Clang Tools.
2. unique_ptr
hides raw new
and delete
A few days before I started writing this text I’ve seen the following Abseil/Google C++ guideline
abseil / Tip of the Week #126: make_unique
is the new new
Plus there are a Core Guideline suggestions:
R.11: Avoid calling new and delete explicitly
The pointer returned by new should belong to a resource handle (that can call delete). If the pointer returned by new is assigned to a plain/naked pointer, the object can be leaked.
In other words: one of the parts of modern C++ is to avoid using raw new
and delete
. Instead, we should wrap allocated memory into RAII objects.
The main reasons why naked new
and delete
might cause harm are the following:
- You need to remember to release the memory. It’s easy when it’s inside a little function… but for larger scopes it gets tricky. Moreover, you might need to interleave with other resources.
- Naked pointers doesn’t show whose the real owner of a resource.
Whenever you see new
try to replace it with a unique_ptr
and make_unique
.
Recently @Modernescpp the author also discusses more rules for allocation and deallocation, so read more here: C++ Core Guidelines: Rules for Allocating and Deallocating (R.13)
3. Control the ownership of a pointer
MyType* pObject;
What can you deduce from the statement above?
And how about:
std::unique_ptr<MyType> pObject;
In modern C++ we head towards using raw pointers as a way to observe things, but the owners are in the form of smart pointers (or other RAII types).
Still, especially in legacy code, you cannot be sure what a raw pointer means. Is it for observation, or it’s an owning pointer?
Knowing who the owner is playing a core part when releasing the resource. It’s especially seen in factory (or builder) functions that return allocated objects:
MyType* BuildObject()
{
MyType* pObj = new MyType();
complexBuild(pObj);
return pObj;
}
// ...
auto pObj = BuildObject();
The only way we had to express who should release the memory was through some guidelines or comments. But the compiler wouldn’t help in case of a bug and a leak.
With:
unique_ptr<MyType> BuildObject()
{
auto pObj = make_unique<MyType>();
complexBuild(pObj.get());
return pObj;
}
// ...
auto pObj = BuildObject();
Now, the ownership is inside pObj
, and it’s released when pObj
goes out of scope.
Passing to functions
Having better control over the ownership of the object is also handy when you want to pass resources to other functions.
Sometimes, when you want to “observe” an object it’s enough to pass a raw pointer or a reference (const reference?) - use .get()
from unique pointer to get the raw pointer, or *ptr
to get the reference.
There are also functions that require full ownership, they are called “sink functions”. In order to pass unique pointer to such function you need to use std::move
.
unique_ptr<MyObj> myPtr ...;
fooAsRawPtr(myPtr.get());
fooAsRef(*myPtr);
fooExclusive(std::move(myPtr));
I wrote about Sink functions a few months ago: Modernize: Sink Functions.
4. Use inside functions
Every time you have to allocate an object on the free store, it’s useful to wrap it intounique_ptr
.
A canonical example:
void FuncMightLeak()
{
MyType* pFirst = new MyType();
if (!process())
{
delete pFirst;
return;
}
MyType* pSecond = new MyType();
if (!processSecond())
{
delete pFirst;
delete pSecond;
return;
}
process();
delete pFirst;
delete pSecond;
}
Do you see how much effort needed to clean up the memory correctly? And it’s such a simple code - in real-life resource management code might be much more complex.
Moreover, as suggested by KrzaQin comments, if process()
throws, then we’ll get a leak, as even our manually crafted deletions won’t save us.
And here’s the converted version that uses unique_ptr
:
void FuncNoLeaks()
{
auto pFirst = std::make_unique<MyType>();
if (!process())
return;
auto pSecond = std::make_unique<MyType>();
if (!processSecond())
return;
process();
}
It looks almost like if the pointers were allocated on the stack.
What’s more, this could also work for other types of resources. So whenever you have to hold such things, you can think about RAII approach (using a smart pointer, custom RAII, custom deleter etc.)
Remark: the examples used memory allocated on the heap… but think if such objects can be allocated on the stack (see my 0th point). Not only it’s more straightforward but usually faster. Heap might be useful for huge objects (that wouldn’t fit into the stack space).
Play with the above code here:
#include <memory>
#include <iostream>
class MyType
{
public:
MyType() { std::cout << "MyType::MyType\n"; }
~MyType() { std::cout << "MyType::~MyType\n"; }
};
bool process() { return true; }
bool processSecond() { return true; }
void FuncMightLeak()
{
std::cout << "FuncMightLeak\n";
MyType* pFirst = new MyType();
if (!process())
{
delete pFirst;
return;
}
MyType* pSecond = new MyType();
if (!processSecond())
{
delete pFirst;
delete pSecond;
return;
}
process();
delete pFirst;
delete pSecond;
}
void FuncNoLeaks()
{
std::cout << "FuncNoLeaks\n";
auto pFirst = std::make_unique<MyType>();
if (!process())
return;
auto pSecond = std::make_unique<MyType>();
if (!processSecond())
return;
process();
}
int main()
{
FuncMightLeak();
FuncNoLeaks();
}
Code @Compiler Explorer
5. Implement “pimpl” idom with unique_ptr
“Pointer to implementation” - “pimpl” is a conventional technique to reduce compile time dependencies. Briefly, it hides all (or some) details of the class implementation with some other type (that is only forward declared). Such technique allows changing the implementation without the need to recompile client code.
Here’s some code:
In the above example NetworkManager
has a private implementation - NetManImpl
which is hidden from the client code (main()
). NetManImpl
is implemented solely in a cpp file, so any changes to that class won’t cause the need to recompile client code - cpp_pimpl_client.cpp
.
While this approach might sound like a lot of redundancy and redirection (see implementations of Connect
or Disconnect
methods that just redirects the calls) it makes modules physically independent.
You can read more about Pimpl here: The Pimpl Pattern - what you should know - C++ Stories
Bonus - 6th reason!
By default, unique pointer uses delete
(or delete[]
) to free the allocated memory. This mechanism is good enough in probably like 99% of cases, but if you like, you might alter this behaviour and provide a custom deleter.
The thing to remember, according to the cppreference:
If
get() == nullptr
there are no effects. Otherwise, the owned object is destroyed viaget_deleter()(get())
. Requires thatget_deleter()(get())
does not throw exceptions.
In other words, the deleter is called when the managed pointer is not null.
Also, as deleter is part of the type of the unique pointer and may contribute to its size (if that’s a function pointer then it will take some space, if it’s only a stateless functor, then not).
Here’s an example:
In the above example, we need to provide a custom deleter to clean up ‘LegacyList’ objects properly.: ReleaseElements
method must be called before the object is deleted. The cleanup procedure is wrapped into a stateless functor object:
struct LegacyListDeleterFunctor {
void operator()(LegacyList* p) {
p->ReleaseElements();
delete p;
}
};
If you like to read more about custom deleters see:
- Wrapping Resource Handles in Smart Pointers
- Custom Deleters for C++ Smart Pointers
- Smart developers use smart pointers (3/7) - Custom deleters - Fluent C++
- Changing deleters during the life of a unique_ptr (4/7) - Fluent C++
If you're interested in smart pointers - have a look at my handy reference card. It covers everything you need to know about unique_ptr
, shared_ptr
and weak_ptr
, wrapped in a beautiful PDF:
Summary
I hope you’re convinced to start using unique_ptr
in your projects :)
With this list, I’ve shown how this type of smart pointers leads to memory safety, code clarity, expressiveness and even much shorter code.
Do you have any cool examples how you used unique_ptr
? Share your stories and observations in the comments below the article.