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
.
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.
#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 pointerT& operator* () {return *_pRes;}
// to mimic a real pointerT* 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:2l2(p);} // >> ---not today:1
int main() {AClass *res = new AClass(); // >> aclass consSmartPointer<AClass> aSmartP(res); // >> SP cons:1l1(aSmartP);} // >> ---Valar Morghulis:0// >> aclass des
Result:
aclass consSP cons:1SP copy cons:2SP copy cons:3---not today:2---not today:1---Valar Morghulis:0aclass des
To briefly explain the code above:
SmartPointer
‘s life-cycle is no more than that of an ordinary class. Thus, logic flow going out of a (function) scope destructs it;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;SmartPointer
is constructed with a valid _pRes
(of type T
), the _refCount
plus 1
;SmartPointer
destructed, in our case, by a logic flow going out of a scope, the _refCount
minus 1
;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
is the std’s implementation of the smart pointer that is more robust than the demo code listed above. And it does not generate dodgy log.
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:
#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 pointerT& operator* () {return *_pRes;}// to mimic a real pointerT* 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:
consdesdesautop(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 the 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:
......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:
consdes
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
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_ptr
s to avoid ambiguity we saw in AutoPointer v2.0. Like automatic pointer, the last owner of the pointer is responsible for deallocation.
#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()); // >> consp1->i = 1;v.push_back(std::move(p1));std::unique_ptr<AClass> p2(new AClass()); // >> consp2->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:
conscons12desdes
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.
“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.
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.
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.
std::auto_ptr
is a
as it can not be easily moved.
If you like this read please clap for it or follow by clicking the buttons. Thanks, and hope to see you the next time.
Originally published at holmeshe.me.