Smart Pointer, shared_ptr, Automatic pointer, and unique_ptr

Objects life-cycle is crucial. A mistake in determining an object’s lifecycle can lead to resource (e.g., memory, fd) leaks as the resource owned cannot be properly released and recycled for future use. When the leak accumulates to a certain level, it crashes the whole system.

Objects life-cycle is also complicated since the ownership of one object might be relinquished by, transferred to, or shared with different entities which include but are not limited to variables, function arguments, modules, data structures, containers, and threads. Again, the resource has to be released and recycled by one of the owners at some undetermined point.

There is no de-facto standard to determine objects life-cycle. Utilities like GC (garbage collection) that is used in Java, ARC used in Objective-C and all those pointers (ptrs) in C++, all have their pros and cons. However, this article is not about pros and cons but is focused on C++ resource management helper classes, Smart Pointer, shared_ptr, auto_ptr and unique_ptr.

Smart pointer

A smart pointer is a wrapper class of a normal pointer. Smart point defines life-cycle with a reference count that reflects how many time the smart pointer object is referenced.
Next, I will show a simple implementation of a smart pointer. The code is for demonstration purposes only, thus, there is no sanity check, no exception handling and no thread-safety guarantee.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <stdio.h>

template < typename T > class SmartPointer {
private:
T* _pRes;
int* _refCount;

void _release() {
if(--(*_refCount) == 0) {
printf("---Valar Morghulis:%d\n",*_refCount);
delete _pRes;
delete _refCount;
} else {
printf("---not today:%d\n",*_refCount);
}
}

public:
SmartPointer() : _pRes(NULL), _refCount(NULL) {
_refCount = new int(0);
printf("SP default cons:%d\n",*_refCount);
}

SmartPointer(T* pRes) : _pRes(pRes), _refCount(NULL) {
_refCount = new int(1);
printf("SP cons:%d\n",*_refCount);
}

SmartPointer(const SmartPointer<T>& sp) : _pRes(sp._pRes), _refCount(sp._refCount) {
(*_refCount)++;
printf("SP copy cons:%d\n",*_refCount);
}

SmartPointer<T>& operator = (const SmartPointer<T>& sp) {
this->_release(); // release the last resource it points to
_pRes = sp._pRes;
_refCount = sp._refCount;
(*_refCount)++;
printf("SP assign:%d\n",*_refCount);
return *this;
}

~SmartPointer() {
this->_release();
}

// to mimic a real pointer
T& operator* () {
return *_pRes;
}

// to mimic a real pointer
T* operator-> () {
return _pRes;
}
};

class AClass {
public:
AClass() { printf("aclass cons\n"); }
~AClass() { printf("aclass des\n"); }
};

void l2(SmartPointer<AClass>& p) {
SmartPointer<AClass> use3 = p; // >> SP copy cons:3
} // >> ---not today:2

void l1(SmartPointer<AClass>& p) {
SmartPointer<AClass> use2 = p; // >> SP copy cons:2
l2(p);
} // >> ---not today:1

int main() {
AClass *res = new AClass(); // >> aclass cons
SmartPointer<AClass> aSmartP(res); // >> SP cons:1
l1(aSmartP);
} // >> ---Valar Morghulis:0
// >> aclass des

Result:

1
2
3
4
5
6
7
8
aclass cons
SP cons:1
SP copy cons:2
SP copy cons:3
---not today:2
---not today:1
---Valar Morghulis:0
aclass des

To briefly explain the code above:

  1. SmartPointer‘s life-cycle is no more than that of an ordinary class. Thus, logic flow going out of a (function) scope destructs it;
  2. SmartPointer has two properties, _pRes and _refCount, both are allocated from heap. Thus, logic flow going out of a (function) scope DOES NOT destruct them;
  3. each time a SmartPointer is constructed with a valid _pRes (of type T), the _refCount plus 1;
  4. each time a SmartPointer is destructed, in our case, by a logic flow going out of a scope, the _refCount minus 1;
  5. however, the destruction of SmartPointer does not necessarily lead to a destruction of _pRes:

a) when _refCount is still larger than 0, SmartPointer simply reduce the _refCount and print

not today

b) only when _refCount is set to 0 by the minus, SmartPointer destructs the resource referred by _pRes and and print

All men must die

So smart pointers work as handles that are used by different parts of a program to keep track and to control the resource instance. When all handles are destroyed, the resource is considered “not used”, and is deleted as well. In the end of this article, I will show some real handles that embody smart pointer in real world.

The sample showcases the usage of smart pointer in program that is linear, which is rarely the case in real scenario. Rather, as mentioned before, the resource (i.e., the instance of AClass) can be shared, by multiple data structure and variables in parallel.

shared_ptr (C++11)

shared_ptr is the std’s implementation of smart pointer that is more robust than the demo code listed above. And it does not generate dodgy log.

Automatic pointer

An automatic pointer, though looks similar to smart pointer, is totally different. It is a convenient helper class that destructs the resource whenever the logic flow going out of the scope, just in case a programmer forgets. To some extent, it makes a pointer (that refers to a memory chunk dynamically allocated in runtime) works similar to a stack variable (statically allocated in compiling time).

Example, AutoPointer v1.0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>

template < typename T > class AutoPointer {
private:
T* _pRes;

public:
AutoPointer() : _pRes(NULL) {}

AutoPointer(T* pRes) : _pRes(pRes) {}

AutoPointer(const AutoPointer<T>& ap) : _pRes(ap._pRes) {}

AutoPointer<T>& operator = (const AutoPointer<T>& ap) {
delete _pRes;
_pRes = ap._pRes;

return *this;
}

~AutoPointer() {
delete _pRes;
}

// to mimic a real pointer
T& operator* () {
return *_pRes;
}
// to mimic a real pointer
T* operator-> () {
return _pRes;
}
};

class AClass {
public:
AClass() { printf("cons\n"); }
~AClass() { printf("des\n"); }
int i;
};

void l1(AutoPointer<AClass>& p) {
AutoPointer<AClass> use2 = p;
}//the resource has already been deallocated here

int main() {
AClass *res = new AClass();
res->i = 5;
AutoPointer<AClass> use1(res);
l1(use1);
}// abort, repeat deallocating pointer

Result:

1
2
3
4
5
6
cons
des
des
autop(1148,0x7fff74eff000) malloc: *** error for object 0x7f9940c03240: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
[1] 1148 abort ./a.out

As given by the code snippet above, automatic pointer works internally like a simplified smart pointer that deallocates the resource regardless of the reference count (in fact, there is no reference count at all).

The coredump shows a major drawback of the automatic pointer: the ownership can not be transferred (to l1() ). As a result, even though the resource has been deallocate in l1(), main()still consider itself as the owner of automatic pointer and deallocates the pointer one time more.

How about implementing the copy constructor as well as the assignment operator so the ownership can be properly transferred?

Example, AutoPointer v2.0:

1
2
3
4
5
6
7
8
9
10
11
12
13
......
AutoPointer(AutoPointer<T>& ap) : _pRes(ap._pRes) {
ap._pRes = NULL;
}

AutoPointer<T>& operator = (AutoPointer<T>& ap) {
delete _pRes;
_pRes = ap._pRes;

ap._pRes = NULL;
return *this;
}
......

Result:

1
2
cons
des

All seems good. Yet it is another example of “fixing one bug leads to another”.

The new problem is that the two semantics, ownership-transferring and copy, are coupled. So it is not compatible to some of the library functions such as std::sort that takes one extra copy (as pivot in quick sort) as it destroys the previous one that is still in use. The detailed explanation of the problem can be found here, and thanks patatahooligan for pointing out the mistake in the original implementation.

std::auto_ptr is the std implementation of the automatic pointer. As discussed above, it is either not very interesting or problematic, so it is now deprecated. And we should use std::unique_ptr instead.

std::unique_ptr (C++11)

std::unique_ptr is the std’s replacement of std::auto_ptr in C++11. With the newly added rvalue and move semantics, the ownership of a unique_ptr can be safely transferred to another entity. Moreover, the copy semantic is disabled for unique_ptrs to avoid ambiguity we saw in AutoPointer v2.0. Like automatic pointer, the last owner of the pointer is responsible for deallocation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <vector>
#include <memory>
#include <cstdio>
#include <fstream>
#include <cassert>

class AClass {
public:
AClass() {printf("cons\n");}
~AClass() {printf("des\n");}
int i;
};

std::vector< std::unique_ptr<AClass> > v;

void l1() {
std::unique_ptr<AClass> p1(new AClass()); // >> cons
p1->i = 1;
v.push_back(std::move(p1));
std::unique_ptr<AClass> p2(new AClass()); // >> cons
p2->i = 2;
v.push_back(std::move(p2));
} // p1 and p2 are not destructed here

int main() {
l1();
for(auto& p: v) printf("%d\n", p->i);
} // >> des
// >> des

Result:

1
2
3
4
5
6
cons
cons
1
2
des
des

As shown in the code snippet above, the unique pointer is preserved across different owners. When the ownership has been moved to vector v, l1() does not deallocates the resource anymore. This gains unique pointer a much wider usage.

N.b., I would rather believe unique pointer is the major reason of the introduction of the new move semantic. Because compared to the improvement gained here, the optimization enabled by move and rvalue is less significant.

Take home

“I can understand the stuffs, but I’m not sure if I still remember them exactly next morning.”

Sure. I will find some real world counterparts to enhance your memory.

1) a std::shared_ptr is like a handle of a video game console.

The console (resource) is “shared” by multiple players with handles, and the game should continue even if there is only one player left. Thus, “Game over” only when all players stop playing.

2) a std::unique_ptr is like a portable game console.

One player at a time, and one should “move” it to let another to play. “Game over” when the LAST player stops playing.

3) a std::auto_ptr is a

as it can not be easily moved.

That's it. Did I make a serious mistake? or miss out on anything important? Or you simply like the read. Link me on -- I'd be chuffed to hear your feedback.