Smart Pointers
C++11 introduces std::unique_ptr
, std::shared_ptr
, std::weak_ptr
and deprecates std::auto_ptr
.
This are for ease the management of memory and other resources.
They remove the need to write custom destructors for nearly all use cases.
Smart pointers are "owning" pointers. They release the resource they point to.
If you do not own the resource, use a "raw" pointer (or a reference).
std::unique_ptr
models exclusive ownership:
-
std::unique_ptr owns the raw pointer
-
there can’t be a second owner (unless misuse)
ownership can be passed to another unique_pointer
by moving
struct bar{ };
void foo(){
std::unique_ptr<bar> uptr0;
{
auto uptr1 = std::make_unique<bar>();
//uptr0 = uptr1; // copy is a comp-time eror
uptr0 = std::move(uptr1); // moves pointer from uptr1 to uptr0
}
}
The owner calls delete
by default, but it is possible to use other deleters too.
struct file_deleter {
void operator()(std::FILE* fp) { std::fclose(fp); }
};
using unique_file = std::unique_ptr<std::FILE, file_deleter>;
auto f = unique_file(std::fopen("file.txt", "rw"));
-
same size of a raw pointer
-
no allocations, or other operation that could fail
-
provides direct access to owned pointer
-
works with incomplete types
struct bar{void foo(){};};
auto pb = new bar();
auto upb = std::make_unique<bar>();
pb->foo();
upb->foo();
if(pb){}
if(pb == nullptr){}
if(upb){}
if(upb == nullptr){}
bar& b = *pb;
bar& ub = *upb;
As unique_ptr
has an interface similar to a normal pointer, it is possible to replace the usage of raw pointers with unique_ptr
iteratively.
During the process, most error are diagnosed by the compiler.
Generally removing the usage of raw owning pointers
-
documents better than a comment who owns the resource
-
permits the compiler to validate assumptions and diagnose errors
-
simplifies code in unexpected ways
-
avoid leaks that are easy to oversee
before
delete ptr;
ptr = new X();
// or
delete ptr;
ptr = newptr;
after
ptr = std::make_unique<X>();
// or
ptr.reset(newptr);
before
for( std::vector<T*>::iterator it = vec.begin(); it != vec.end(); ++it ){
delete *it;
}
vec.clear();
after
vec.clear();
or even
before
struct foo {
using internal_map = std::map<std::string, I*>;
using external_map = std::map<std::string, internal_map>;
external_map m_map;
~foo() {
for ( auto it = m_map.begin(); it != m_map.end(); ++it ) {
for ( auto subIt = it->second.begin(); subIt != it->second.end(); ++subIt ) {
delete subIt->second;
}
}
void bar(const std::string& str1, const std::string& str2, I* i) {
auto it = m_map.find(str1);
if ( it != m_map.end() ) {
auto subIt = it->second.find(str2);
if ( subIt != it->second.end() ) {
delete subIt->second;
}
it->second[str2] = i;
} else {
internal_map imap;
imap[str2] = i;
m_map[str1] = imap;
}
}
}
// ...
};
after
struct foo {
using internal_map = std::map<std::string, std::unique_ptr<I>>;
using external_map = std::map<std::string, internal_map>;
external_map m_map;
void bar(const std::string& str1, const std::string& str2, I* i) {
m_map[str1][str2].reset(i);
}
// ...
};
Without unique_ptr
or similar alternatives
-
enlist every place where a resource is allocated
-
track where handles to the resources are passed
-
verify that every resource is closed only once
-
verify that the correct function for closing the resource is used
It’s hard and error prone to apply those guidelines consistently
C* foo();
Should we free/close the return value of foo
.
Do we own it?
If yes, how?
void bar(C*);
void foo(){
auto i = new C();
bar(i);
}
Is it correct that no delete takes place?
Does it happen inside bar
?
void bar(C*);
void foo(){
auto i = new C();
bar(i);
delete i;
}
Is i
always released?
If not, is it correct?
void bar(C*);
void foo() {
auto i = new C();
try {
bar(i);
} catch (...) {
delete i;
throw;
}
delete i;
}
Is very verbose and error-prone.
void bar(C*, C*);
void foo() {
auto i = new C();
try {
auto j = new C();
try {
bar(i, j);
} catch (...) {
delete j;
throw;
}
delete j;
} catch (...) {
delete i;
throw;
}
delete i;
}
It does not scale, no-one writes code like that. Imagine a function or constructor with 3 parameters.
With unique_ptr
or similar constructs
-
Newer use non-owning handles for owning resources
-
verify if and where
.release()
is used, as it is equivalent to manage resources manually
Which is ideally equivalent to
-
Do not handle resources manually
std::unique_ptr<C> foo();
C* foo();
In both cases, from the function signature its clear if we own the returned value or not.
In both cases, calling foo()
does not cause any leak by design.
void bar(std::unique_ptr<C>);
void foo(){
auto ptr = std::make_unique<C>();
bar(std::move(ptr));
}
or
void bar(C*);
void foo(){
auto ptr = std::make_unique<C>();
bar(ptr.get());
}
In both cases it’s clear if bar
takes ownership of the parameter or not, there is no need to track about who owns the pointer.
It also scales; the written code does not depend on the number of execution paths.
struct bar {
std::unique_ptr<C> i;
std::unique_ptr<C> j;
explicit bar() :
i(std::make_unique<C>()) ,
j(std::make_unique<C>()) {
}
// ~bar is compiler-generated
};
bar
is leak-free.
As a corollary of the single-responsibility principle: A class should own directly only one resource.
.release()
is mostly necessary when working with external or C libraries, as we cannot change those functions to use unique_ptr
.
It is possible to wrap/hide those functions (which are a small subset of the code we control) and make them work with unique_ptr
too.
Supposing that third-party code is correct, it possible to keep track of places where .release()
is needed to a very limited subset of code, and ensure the absence of leaks with a grep
.
before
auto attr_p = xmlGetProp(rNode, "p");
after
struct free_xml_generic {
void operator()(void* handle) { xmlFree(handle); }
};
template <class T>
using unique_xmlPtr = std::unique_ptr<T, free_xml_generic>;↲
using unique_xmlChar = unique_xmlPtr<xmlChar>;↲
inline unique_xmlChar get_unique_xmlProp(const xmlNode* node, const xmlChar* name) {
return unique_xmlChar(xmlGetProp(node, name));
}
#pragma GCC poison xmlGetProp
auto attr_p = get_unique_xmlProp(rNode, "p");
Did not talk much about std::shared_ptr
and std::weak_ptr
because there are not as many use cases as with std::unique_ptr
.
It has a copy constructor, every copy increments a thread-safe counter:
-
it is more difficult, if possible, to understand who owns the resource
-
can cause leaks with circular dependencies
-
has non-trivial overhead compared to a raw pointer
-
the synchronisation is costly if not needed
-
it is trivial to convert an
unique_ptr
to ashared_ptr
, generally impossible to do the opposite
struct bar{ };
// bad, forces the user to allocate bar on the heap,
// even if foo cannot use that information
void foo(const std::unique_ptr<bar>&);
// better
void foo(const bar*);
// even better if bar cannot/should not be null
void foo(const bar&);
// even better if bar is "small", like integral types, enums or small data structures, like string_view, span, ...
void foo(bar);
// bad, leaks implementation details that the caller cannot use
const std::unique_ptr<T>& foo();
// better
T* foo();
// even better if return value cannot/should not be null
const T& foo();
class X {
std::unique_ptr<int> i;
public:
explicit X(std::unique_ptr<int> ii) : i(std::move(ii)) {}
};
void bar(std::unique_ptr<int>);
void foo(){
auto i = std::make_unique<int>(42);
X x(std::move(i));
bar(std::move(i));
bar(std::make_unique<int>(42));
}