When we develop our program or the system continues to grow as time
goes by, memory leakage is usually a pain we suffer
most. To militate against this problem, C++ has introduced smart pointer
family - unique_ptr
, shared_ptr
,
weak_ptr
, defined in header <memory>
,
since C++11.
Precisely,
unique_ptr
inherits most of characteristics fromauto_ptr
, which has been implemented since C++03. Yet, due to the lack ofmove
semantics, it encounters significant limitation:Note that
1
2
3
4
5 auto_ptr<int> p (new int(1));
vector<auto_ptr<int>> vec;
vec.push_back(p); // !!! error, because of Copy constructor
vec.push_back(std::move(p)) // Okay, with move semantics since C++11auto_ptr
is deprecated now and should not be used any more for safety.
A Toy Example
Let's use a simple example to illustrate how these smart pointers work:
1 | class Obj { |
When the object is created or released, the corresponded message will be printed.
Original Way with
new
and delete
Before utilising smart pointers, developers who want to allocate a new object might write the below code:
1 | Obj* p0 = new Obj("0"); |
We have to carefully ensure the created object will be released before leaving the procedure; otherwise leading to memory leakage. However, relying on smart pointers, the system is able to automatically destroy the allocated object based upon whether it is owned by any pointer.
unique_ptr
unique_ptr
, as its name suggests, owns an object
uniquely. That means you cannot have the same memory owned by two or
more unique_ptr
objects; thus, it is also not
copyable. When the unique_ptr
goes out of scope,
its holding object will be destroyed immediately.
Code Snippet from
std::unique_ptr
The implementation of unique_ptr
in libstdc++ is worth
mentioning for clarity (I have omitted the detail):1
1 | template <typename _Tp, typename _Dp = default_delete<_Tp>> |
Briefly, unique_ptr
will release the current holding
memory if necessary. Moreover, its copy constructor and copy assignment
are disabled.
Example 1:
unique_ptr
is unique and not copyable
1 | int main() |
Output: Construct 1
Hi 1!
<<... END ...>>
Destroy 1
The allocated space held by p1
is released automatically
when its lifetime ends.
It is recommended to use
make_unique
to createunique_ptr
objects.
Example 2: original holding object will be destroyed before setting to null
1 | int main() |
Output: Construct 1
Hi 1!
Destroy 1
<<... END ...>>
p1
has been destroyed before setting it as null. Using
the method reset()
can achieve the same result as well.
Example 3: original holding object will be destroyed before move assignment
1 | int main() |
Output:
Construct 1 |
Object 2
has been destroyed while moving object
1
into p2
. Note that you have to release the
ownership of object 1
with release()
firstly
before assigning it to the next owner.
The Advantages of
make_unique
over new
operator
make_unique
has been introduced since C++14, and its
implementation can be briefly rewritten as:2
1 | template<typename T, typename... Args> |
For the most part, it makes no difference between either
new
or make_unique
:
1 | unique_ptr<int>(new int(1)); |
However, the former may go awry if it involves several arguments evaluation in a function call:3
1 | func(unique_ptr<int>(new int(1)), Foo()); // unsafe |
Before C++17, the order of function arguments evaluation is not defined.4
Consider the evaluation sequence below:
- Create
int (1)
object --- Success ! - Execute
Foo()
--- Exception ! - Assign the created
int (1)
to aunique_ptr
object --- Cannot be performed !
If new
has been processed without assigning the newly
created object to unique_ptr
, and exception happens in
Foo()
, then memory leakage occurs because there is no way
for unique_ptr
object to access the newly created object to
destroy it.
Therefore, when you would like to create unique_ptr
objects, make_unique
is a better choice for exception
safety. Even though it is not introduced in C++11, defining it manually
is quite simple as aforementioned.
Construct an Array of
unique_ptr
Objects
There are two ways to construct an array of unique_ptr
objects as well.
1 | int main() |
Output: Construct DEFAULT
Construct DEFAULT
Construct DEFAULT
Hi DEFAULT!
Hi DEFAULT!
Hi DEFAULT!
<<... END ...>>
Destroy DEFAULT
Destroy DEFAULT
Destroy DEFAULT
Compared to a single object, the parameter in
make_unique
is the array size instead of
an initialiser.
Similar to the single object, its array version can be briefly rewritten as:
1 | template<typename T> |
Note the subtle difference against a single object is the judgement
of is_array<T>::value
.
shared_ptr
Unlike unique_ptr
, the ownership of an object can be
shared by multiple shared_ptr
objects. The object will be
destroyed once it is not owned by any shared_ptr
objects.
This information can be obtained by the method
use_count()
Code Snippet from
std::shared_ptr
The implementation of shared_ptr
in libstdc++ is worth
mentioning for clarity (I have omitted the detail):5
1 | template<typename _Tp> |
shared_ptr
inherits from __shared_ptr
class6, and you could observe
weak_ptr
is able to access the private members in
shared_ptr
objects. Actually, shared_ptr
and
weak_ptr
indeed cooperate together from time to time which
will be elaborated later.
1 | template<typename _Tp, _Lock_policy _Lp> |
Briefly, shared_ptr
object records the number of owners
(__shared_count
) as well as the pointer of the managed
object. Besides, __shared_ptr_access
, the parent class,
defines whether this object should be accessed by array operators or
not.
1 | template<_Lock_policy _Lp> |
The __shared_count
object is to manage the information
of reference count. Here, __weak_count
class is able to
access the private members of __shared_count
. Most of
operations on the pointer itself are specified in
_Sp_counted_base
.
1 | template<_Lock_policy _Lp = __default_lock_policy> |
Note that while a shared_ptr
object is created, the
lifetime of the managed object and its control block
might not be the same. The former is based upon whether it is shared by
any shared_ptr
objects, whereas the later is
weak_ptr
.
Control block contains the information about how to allocate or deallocate the managed object, e.g. its allocator and deleter.
Example
1: several shared_ptr
can share the same object
1 | int main() |
Output: Construct 1
count = 1
count = 2
<<... END ...>>
Destroy 1
Object 1
can be held by both sp0
and
sp1
. It is recommended to use make_shared
to
create the smart pointer due to the same reason explained in
make_unique
.
Example
2: the managed object will be destroyed when it is not held by any
shared_ptr
1 | int main() |
Output: Construct 2
count = 2
count = 1
Destroy 2
count = 0
<<... END ...>>
Object 2
is destroyed while its reference count becomes
0.
Construct an Array of
shared_ptr
Objects
Unlike unique_ptr
objects, shared_ptr
cannot dynamically allocate an array via make_shared
until
C++20. Moreover, prior to C++17, developers ought to specify the deleter
to manage dynamically allocated arrays.7
1 | int main() |
Output: Construct DEFAULT
Construct DEFAULT
Construct DEFAULT
Hi DEFAULT!
Hi DEFAULT!
Hi DEFAULT!
<<... END ...>>
Destroy DEFAULT
Destroy DEFAULT
Destroy DEFAULT
Actually, it is worth mentioning that since GCC 7.5, a default deleter to handle the dynamically allocated array has been provided by
struct __sp_array_delete
:
1
2
3
4
5
6 // The default deleter for shared_ptr<T[]> and shared_ptr<T[N]>.
struct __sp_array_delete
{
template<typename _Yp>
void operator()(_Yp* __p) const { delete[] __p; }
};It works as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 template<typename _Tp, _Lock_policy _Lp>
class __shared_ptr : public __shared_ptr_access<_Tp, _Lp>
{
// ...
// Constructor will check whether it is an array
template<typename _Yp, typename = _SafeConv<_Yp>>
explicit
__shared_ptr(_Yp* __p)
: _M_ptr(__p), _M_refcount(__p, typename is_array<_Tp>::type())
{
// ...
_M_enable_shared_from_this_with(__p);
}
};The
__shared_ptr
constructor checks whether the managed object is an array.
1
2
3
4
5
6
7
8
9 template<_Lock_policy _Lp>
class __shared_count
{
// ...
template<typename _Ptr>
__shared_count(_Ptr __p, /* is_array = */ true_type)
: __shared_count(__p, __sp_array_delete{}, allocator<void>())
{ }
};If the pointer is an array, the default deleter will be designated as
__sp_array_delete
.Thus, the below code should work fine even if we do not provide a deleter:
1
2
3
4
5
6 int main()
{
shared_ptr<Obj[]> spArray (new Obj[3]); // Okay if deleter is not provided
// ...
return 0;
}However, prior to C++17, we still suggest developers follow the rule to avoid undefined behavior resulted from compiler dependency.
Leakage caused by Circular
Reference within shared_ptr
Consider the case when you would like to implement list data structure, the code might look like as follows:
1 | struct List { |
And the list might form a cycle:
1 | int main() |
Output: Construct node 1
Construct node 2
count = 2
<<... END ...>>
Under this circumstance, both node 1
and
node 2
are referenced to one another, the user count never
degrades to zero even when the program terminates, leading to memory
leakage. This problem is called circular reference or
cyclic dependency issue.
To solve this problem, weak_ptr
could come into play,
which won't affect the reference count of an object. We may simply
modify our list structure as follows:
1 | struct List { |
Output: Construct node 1
Construct node 2
count = 1
<<... END ...>>
Destroy node 2
Destroy node 1
Now, with the help of weak_ptr
, the reference count does
not increase for the reason that it is not considered an owner for this
object. The memory leakage issue can be prevented.
weak_ptr
weak_ptr
can hold a "weak" reference to an object, which
won't affect the reference count managed by shared_ptr
. The
main aim of weak_ptr
is to own a temporary ownership so we
can track this object. It is also an effective way to prevent the memory leakage problem
caused by a cycle within shared_ptr
objects. In
addition to that, we should be aware that even when the object held by
shared_ptr
is destroyed, the lifetime of its control block
might be extended. To access the referenced object of
weak_ptr
, users should use the method lock()
to get its original shared_ptr
object first.
Code Snippet from
std::weak_ptr
Let's take a look at the implementation in libstdc++ (detail is omitted):8
1 | template<typename _Tp> |
weak_ptr
inherits from __weak_ptr
and using
lock()
would help the user get a converted
shared_ptr
object.
1 | template<typename _Tp, _Lock_policy _Lp> |
Briefly, weak_ptr
can be used to track the managed
object held by shared_ptr
. It also has its own weak
reference count.
1 | template<_Lock_policy _Lp> |
The mechanism of the reference counter in weak_ptr
is
quite similar to shared_ptr
. Yet, weak_ptr
mainly operates upon weak reference.
1 | template<_Lock_policy _Lp = __default_lock_policy> |
_M_use_count
is used by shared_ptr
to store
the strong reference count, whereas _M_weak_count
is used
by weak_ptr
to store the weak reference count.
1 | template<> |
Once the weak reference count reaches zero, the control block will be deallocated.
Example
1: weak_ptr
does not increase the reference count held by
shared_ptr
1 | int main() |
Output: 1
2
3
4
5
6
7
8Construct 1
count = 1
count = 1
<<... END ...>>
Destroy 1
Aside from this, wp.use_count()
returns the number of
shared_ptr
that manages this object; thus the value should
be the same as sp.use_count()
.
Example 2: access the
object through lock()
1 | int main() |
Output: 1
2
3
4
5
6
7Construct 1
Hi 1!
<<... END ...>>
Destroy 1
Users should use lock()
to get the
shared_ptr
pointer to access this object.
Example
3: use expired()
to check the availability of an
object
1 | int main() |
Output: 1
2
3
4
5
6Construct 1
Destroy 1
Object has been destroyed!
<<... END ...>>
expired()
is equivelent to
use_count() == 0
.
Example 4:
release the reference from weak_ptr
1 | int main() |
Output:
Construct 1 |
You cannot assign a weak_ptr
object as null
directly.
Construct an Array of
weak_ptr
Objects
You cannot construct an array of weak_ptr
objects as the
approach in unique_ptr
and shared_ptr
because
the operator []
to access the array elements is not
defined.
1 | weak_ptr<Obj[]> wpArray; // ??? wpArray[i] is not defined |
A workaround is to declare it as a regular array, then tackle its elements separately:
1 | weak_ptr<Obj> wpArray[3]; |