Download - smart pointer

Transcript
Page 1: smart pointer

Advanced Concepts in C++

Garbage Collection Techniques in C++Smart Pointers for automated memory management

Simon Fuhrmann

Darmstadt University of Technology, Germany

September 13, 2005

Abstract: This paper deals with library implemented automated dynamic memory manage-ment, also known as automated garbage collection (or just AGC or GC for short) for the C++programming language.

Contents

1 Introduction 1

2 Motivation 22.1 Objects as return value . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22.2 Objects in GUI environments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4

3 Understanding smart pointers 53.1 Wrapping pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53.2 Implemented idioms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63.3 Resource management . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73.4 Strategies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8

4 Reference counting smart pointers 104.1 Reference counting implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . 104.2 Reference counting issues . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

4.2.1 Cyclic references . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154.2.2 Multi-Threading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184.2.3 Performance analysis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

5 Smart pointer libraries 225.1 The C++ Standard Library . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225.2 The Boost Library . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225.3 The Loki Library . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22

A ref ptr source code 24

List of Figures 28

I

Page 2: smart pointer

1 INTRODUCTION 1

1 Introduction

Managing dynamic memory is a complex and highly error-prone task and is one of the mostcommon causes of runtime errors. Such errors can cause intermittent program failures that aredifficult to find and to fix.

There are different types of errors with managing dynamic memory. One of the most commonmistakes are memory leaks. Memory leaks occur if a program fails to free objects that are nolonger in use. Typically, a program continues to leak memory, and over the time its performancedegrades and it can eventually run out of memory. Another problem are premature frees. Thisis caused by freeing objects that are still in use, which can result in corrupted data and may leadto sudden failures. Memory fragmentation is caused by frequent allocation and deallocation ofmemory such that larger chunks of memory are divided into much smaller, scattered pieces, andthe memory between these pieces is too small to be reallocated again.

Memory management and C++

The most critical issues are memory leaks and premature frees. Every instanciated primitive type(int, char, etc.) and complex type (objects) needs memory, but not all of the memory is managedmanually. The place where the data resides is decisive: Nearly every primitve instance is createdon the stack, even most of the complex instances are created on the stack. Since the stack hasautomatic extent, the requested data will be automatically reclaimed if it goes out of scope. Butdata that is created on the heap will stay until it is freed, so the heap has dynamic extent.

In the Java programming language every object needs to be created on the heap, and typical Javacode looks like this:

public void myMethod()

{

MyClass obj = new MyClass();

// Do something with obj

}

Sure, obj will go out of scope at the end of myMethod but the data will not be freed since it wascreated on the heap. The stack can only be used for primitive types. And this is one of the mainreasons for Java’s completely different garbage collection model. Java uses a mark-sweep garbagecollector to detect and reclaim unused data.

In the C++ programming language there is no such garbage collector. Objects can be created onthe heap, like in Java, and also on the stack, like any other primitive type:

void my_function()

{

MyClass obj;

// Do something with obj

}

Cleaning up obj is not neccessary because the stack reclaims all data when the scope is exited.This simplifies memory management a lot and no garbage collector is needed in most cases.

It is advisable to use the stack as often as possible, not only because there is no need to care aboutmemory management but also because the stack is faster than the heap. On most operatingsystems the stack has fixed size and no memory allocations need to be done. It’s just a matter ofdecrementing and incrementing the stack pointer.

It’s even possible to use STL containers with non-pointer types which eliminates some more needsof manual memory management. If the STL container’s scope is exited all objects within thatcontainer are reclaimed automatically. But only non-pointer types are reclaimed, the data ofstored pointers is untouched!

Page 3: smart pointer

2 MOTIVATION 2

2 Motivation

In real life applications manual memory management is an issue and cannot be bypassed completelybecause some situations require manual memory managment. There are several reasons for thatbut the most important ones are:

• The stack is limited, on most operating systems to somewhat in between 8 and 64 MBand some big objects will not fit. But in most cases big data has dynamic size and needsto be created on the heap, anyway, so the corresponing class needs to care about memorymanagement, and not the creator of the instance. An object obj requires sizeof(obj) bytesof the stack (if it was created on the stack).

• More important are objects that need to outlast the current function scope. There are a lotscenarious for this issue. Most common are objects in GUI environments and objects thatsould be used as return value.

2.1 Objects as return value

Using objects as return value is relatively common. In most cases the object can be simply returnedif there are no performance considerations.

Approach 1: Copy return value from local variable:

std::string repeat_string(const std::string& str, int times)

{

std::string result;

for (int i = 0; i < times; ++i)

result += str;

return result;

}

The function repeat string() takes a string and an amount of repetitions and returns times

concatenated copies of str. On the one hand, the code is simple and easy to understand, and ifstr and times are small, copying the result should not be matter. But on the other hand if stris long, times has a big value, and performance is critical, this code is very expensive.

Note: This very simple example may get optimized by the compiler with a technique called ”returnvalue optimization”. This optimization requests space for the result in the right place and skipscopying the result (skips calling the copy constructor). But more complex function calls may notget optimized.

Approach 2: Supply result space for the new data:

void repeat_string(const std::string& str, int times,

std::string& result)

{

for (int i = 0; i < times; ++i)

result += str;

}

This approach uses a trick to prevent copying the result from a local variable. There are no moreperformance considerations and memory management is not an issue here. But other reasons ratesthis solution far from beeing good. Using parameters for values that should be return values isunintuitively. But unfortunately, this is common practice in functions from libc, the C library.

Page 4: smart pointer

2 MOTIVATION 3

Even worse, there is no control over the creation process of the result object and there is noguarantee that result was created with appropriate constructor parameters. Quickly forgettingthis approach, it’s time to forge ahead with some better ideas.

Approach 3: Return a pointer to the data:

std::string* repeat_string(const std::string& s, int times)

{

std::string* result = new std::string;

for (int i = 0; i < times; ++i)

*result += s;

return result;

}

Returning a pointer to the data is the most convenient way to perform a cheap copy operation.Only few bytes (the actual size of a pointer depends on the machine) need to be copied. But thereis no way to reclaim the requested memory within the repeat string() function. The deletion ofthe data need to be done outside this function, after the result has been used. Another idea wouldbe using a static buffer that gets overwritten on subsequent use to avoid deletion. But neitherremembering to delete the data nor supplying a static buffer is a good solution for that simpleproblem.

Approach 4: Return memory-managed data:

This approach is a little lookahead to smart pointers and possibly the only efficient way withautomated memory management:

auto_ptr<std::string> repeat_string(const std::string& s, int times)

{

auto_ptr<std::string> result(new std::string);

for (int i = 0; i < times; ++i)

*result += s;

return result;

}

The result ist created in the heap, of course, because the data need to exceed the current scope.But instead of leaving the cleanup to the caller, a smart pointer called auto ptr takes care aboutthe deletion of the data. The specific semantics from auto ptr will be explained later on. Theslightly complicated code (that uses C++ templates) can be cleaned with typedef’s and results ina code like this:

typedef auto_ptr<std::string> StrPtr;

StrPtr repeat_string (const std::string& s, int times)

{

StrPtr result(new std::string);

for (int i = 0; i < times; ++i)

*result += s;

return result;

}

As a result, the code looks identical to the first approach but some things are different: The dataneeds to be created with new and the result needs to be dereferenced to access the data, that’sbecause a smart pointer behaves like a dump pointer.

Page 5: smart pointer

2 MOTIVATION 4

2.2 Objects in GUI environments

The most GUI frameworks come with their own main loop. This means that the control flowresides in the loop of the GUI framework to care about things like redrawing the GUI or waitingfor user actions. The programmer is able to attach some callback functions to interesting events(e.g. a button click). These callback functions need to return within a short time to let the mainloop resume its work. If a callback function does not return, the GUI freezes and cannot respondto user actions, because the main loop is not active.

Objects that are created in callback functions may be persistent and need to outlast the functionscope. As a consequence, these objects cannot be created in stack.

The following code should serve as an example. The callback function create window() is calledwhen the user clicks a button to create a new window. The first piece of code is an example ofhow it goes completely wrong:

void create_window (void)

{

/* Creating the window. */

Window mywin("Window Title");

/* Creating two buttons. */

Button close_but("Close");

Button ok_but("Ok");

/* Adding the buttons to the window. */

mywin.add_widget(close_but);

mywin.add_widget(ok_but);

/* Let the window pop up. */

mywin.show_all();

}

Ok, whats wrong with this code? Like mentioned before, the GUI toolkit has its own main loop.Until the main loop is not active, no windows will be drawn and the GUI will not respond touser interaction. In the example above the window and the buttons are created in the local scopeand if the scope exits, the window and the buttons will be destroyed again. It’s relatively easyto understand that the code above does absolutely nothing but creating and destroying widgets,that will not be drawn by the GUI toolkit.

The second scrap of code will work, but the problem is obvious: The acquired memory need to bedeleted somewhere.

void create_window (void)

{

/* Creating the window. */

Window* mywin = new Window("Window Title");

/* Creating two buttons. */

Button* close_but = new Button("Close");

Button* ok_but = new Button("Ok");

/* Adding the buttons to the window. */

mywin->add_widget(*close_but);

mywin->add_widget(*ok_but);

/* Let the window pop up. */

mywin->show_all();

}

After returning from the callback function the main loop can do its work and display the windowwith its content.

Page 6: smart pointer

3 UNDERSTANDING SMART POINTERS 5

3 Understanding smart pointers

Smart pointers are objects that look and feel like dump pointers, but are smarter.

To obtain the look and feel of a dump pointer, a new type of pointer has to be introduced. Andto be smarter than a dump pointer, a few things need to be defined. First, a technique or astrategy needs to be declared. Then, some place or environment for the implementation of astrategy need to be created. Luckily, classes offer all the functionality needed to implement smartpointers with different pointer types and strategies. The creation of the environment where a dumppointer can be embedded (which is the process of creating a class with approprate constructorsand operators) is called ”wrapping a pointer”. This process is independent of any strategy thatcan be implemented later on.

3.1 Wrapping pointers

In the C-library the type void* is used as generic pointer type. But member functions cannot becalled without a type and user typecasts would be required to operate on the pointer’s data. WithC++ templates the user can constitude the type of dump pointer to be stored inside the wrappedpointer instance.

Constructors

To get the dump pointer inside the wrapped pointer a few constructors are required. Null pointersshould indicate an undefined destination for smart pointers like they do for dump pointers.

• A null smart pointer can be created with the default constructor that takes no arguments.

Example: SmartPtr<std::string> ptr;

• A copy constructor need to be defined, to initialize a wrapped pointer from anothercompatible wrapped pointer instance.

Example: SmartPtr<std::vector> ptr(ptr2);

• To initialize the wrapped pointer from a dump pointer a constructor is needed with the typeof the dump pointer as argument.

Example: SmartPtr<int> ptr(new int(31337));

Operators

Operators are needed to implement the syntax of a dump pointer. The operators of interest arethe following, where ptr1 and ptr2 are wrapped pointers:

• The dereference operator returns a reference to the data.

Example: Object& o = *ptr1;

• The pointer to member operator (for direct member access) returns a pointer to the data.

Example: ptr1->my_method();

• The assignment operators reassign the pointer to the data from a dump pointer or an existingsmart pointer.

Example: ptr1 = new Object();

ptr1 = ptr2;

• The comparison operators compare the data pointer with another data pointer of a givensmart pointer or dump pointer.

Example: if (ptr1 == ptr2) { ... }

if (ptr1 != ptr2) { ... }

Page 7: smart pointer

3 UNDERSTANDING SMART POINTERS 6

Class outline

The following outline of a class for wrapped pointers declares all discussed features. The definitionof these methods is trivial and can be looked up in the appendix of this article. Note that thedefinitions of the assignment operators and the destructor depend on the strategy that is used forthe smart pointer. The explicit keyword is explained just below.

template <class T>

class MyPointer

{

private:

T* ptr;

public:

MyPointer (void);

explicit MyPointer (T* p);

MyPointer (const MyPointer<T>& src);

~MyPointer(void);

T& operator* (void) const;

T* operator-> (void) const;

MyPointer<T>& operator= (T* rhs);

MyPointer<T>& operator= (const MyPointer<T>& rhs);

bool operator== (const MyPointer<T>& rhs) const;

bool operator!= (const MyPointer<T>& rhs) const;

};

The explicit keyword

A constructor taking a single argument is by default an implicit conversion operator.

What this means can be easily explained with a small example. Imagine a function

void f(std::string s)

that takes a string. Calling this function with f("Hello") is absolutely ok because "Hello"

(which is of type char*) gets implicitly converted to the type std::string because there is animplicit constructor like std::string(char* str).

If such a conversion is not welcome the constructor should be defined as explicit.

In the smart pointer’s case an implicit conversion from an unmanaged pointer to a smart pointeris to be prevented to avoid accidental deletion of the data and mental aberration.

3.2 Implemented idioms

RAII

RAII is short for Resource Acquisition Is Initialization and relies on the life-cycle of objects: Theconstructor is called to initialize the object and the destructor is called when it goes away. Objectsare usually created either automatically in a local scope, in which case they are destroyed whenthe scope is exited (via normal flow, return, or exception), or they can be created with new inwhich case they are destroyed by an explicit delete.

If the resource in question (a file, lock, window, etc.) is acquired (opened, locked, created) ina constructor, and released (closed, unlocked, disposed) in the destructor, then the use of the

Page 8: smart pointer

3 UNDERSTANDING SMART POINTERS 7

resource is tied to the lifetime of the object. Since object lifetimes are well-defined, so is the useof the resource.

The following example demonstrates elegant use of RAII by opening and closing a file:

void read_file (void)

{

File myfile("/foo/bar.txt");

while (!myfile.eof())

// Do some reading

}

This code opens the named file within myfile’s constuctor, operates on the open file in the while-loop and closes the file in myfile’s destructor. So, the code does what it’s intended to do withoutmanually closing the file, which saves time and errors.

In C++, newed objects has dynamic extent and must be explicitly deleted, but in many cases theRAII idiom can be used to automate this. This is the place where smart pointers becomes handy.

Typically, memory will be requested and passed to the constructor of a smart pointer. Later onthe managed data will be released in ”the right” destructor. The means of ”the right” are definedin the smart pointer’s semantics. ”The right” destructor for the reference counting or referencelinking smart pointer is the one from the last referencing smart pointer.

The Proxy Pattern

The Proxy Pattern (which is also known as Surrogate) provides a placeholder for another objectto control access to it. The Proxy Patterm is described in ”Design Patterns” from Gamma et al.

The classic example is about a text document with several images in it. The images are storedon hard disc and should not all be loaded at once every time the document is opened. Insteadthe runtime objects representing the images should obay some lazy initialization semantic like ”animage gets loaded if it isn’t already loaded and if it becomes visible through navigation in thedocument”.

The solution is a proxy object for each image object which implements the lazy initialization andloads the image if it’s needed. So the image proxy is a placeholder for the image object.

A smart pointer is a proxy object for the bare data pointer which does not implement any lazyinitialization but smart destruction of the data and there are different strategies for smartness. Butsmart proxies are not limited to memory management. Even other types of resource managementcan be implemented.

3.3 Resource management

The concept of smart pointers (the proxy concept) can address more problems than just memorymanagement. Neary all resources that do not implement the RAII idiom can be wrapped intoproxy objects to reduce scattered resource management code and programming errors. But whatis a resource in question? A resource is anything the program has to acquire and release afterusing it.

The owner of a given resource is a piece of code that is responsible for its release. This ownershipfalls into two classes: Automatic and explicit ownership. A resource is owned automatically if itsrelease is guaranteed by some mechanisms of the language. But the problem starts with resourcesthat need to be explicitly released.

Page 9: smart pointer

3 UNDERSTANDING SMART POINTERS 8

One of the prominent examples for explicit ownership is dynamic memory, which has to be acquiredwith new and need to be released with delete. But there are other explicit owned resources likefile handles, sockets and critical sections.

• File handles can be wrapped into smart file handles that close themself if there are no morereferences. This may be useful for logging file handles which are scattered throughout thewhole code.

• Networking sockets can be wrapped as smart sockets. Like smart file handles, a socket canbe closed if there is no more referencing object. And in fact file handles and networkingsockets are both file descriptors (on Unix-like operating systems, at least).

• An exclusive area or critical section needs to be locked when the flow enters a specific part ofthe code and unlocked again if the critical section is exited. The program tends to deadlockif a critical section is never exited due to programming errors. In this case it’s useful to bindthe lifetime of a lock to the lifetime of a scoped object. So the critical section is guaranteedto be released by the mechanisms of the language.

The most common strategy for resource management is reference counting and, for some resources,reference counting is the only appropriate strategy. But there are others particularly with regardto memory management.

3.4 Strategies

The created wrapped pointer class offers no more functionality than any dump pointer. To addsome smartness to the code skeleton a strategy needs to be implemented. Before going into animplementation some strategies will be discussed. The list of strategies is not complete but shouldbe sufficient for most applications.

Every strategy is entirely defined by the characteristics of the assignment operators and the de-structor. Let ptr1 and ptr2 be smart pointers. Possible strategies for ptr1 = ptr2 and reset()

are described for each strategy.

Scoped pointer

The scoped pointer is rarely used. It stores a pointer to a dynamically allocated object. Theobject pointed to is guaranteed to be deleted, either on destruction of the scoped pointer, orvia an explicit reset(). The assignment ptr1 = ptr2 is forbidden. Since a scoped pointer isnoncopyable it is caved within its scope and cannot get out. It’s safer than the reference countingor ownership transfer pointers for pointers which should not be copied.

Example: A local object (e.g. a class member) should be protected from being accidently passedto the outside of the class scope.

Copied pointer

The copied pointer stores a pointer to a dynamically allocated object that is guaranteed to bedeleted on destruction of the copied pointer or via explicit reset(). The assignment ptr1 =

ptr2 creates a new copy of the object pointed by ptr2, and let ptr1 point to this copy. Creatingthe new copy can be done with memcpy() (for plain old data (POD), like any C primitive) or byusing some ”clonable” concept (for user defined types (UDT), like C++ classes), which has to beimplemented by the element type.

Example: The copied pointer can be used whenever stack semantics are needed but the objectcan not be created on the stack for some reason.

Page 10: smart pointer

3 UNDERSTANDING SMART POINTERS 9

Ownership transfer pointer

The ownership transfer pointer stores a pointer to a dynamically allocated object that is guaranteedto be deleted on destruction of the ownership transfer pointer or via explicit reset(). Theassignment ptr1 = ptr2 transfers the ownership or responsibility for cleaning up the allocateddata from ptr2 to ptr1. This makes ptr2 a null pointer. Taking back the ownership can beaccomplished by using the release() operation.

Example: The ownership transfer pointer is a lightweight pointer and can be used in every daycode, for example to return object pointers from functions (see Motivation for details).

Reference counting pointer

The reference counting pointer stores a pointer to a dynamically allocated object that is guaranteedto be deleted on destruction of, or via explicit reset() on the last referencing pointer for oneobject.

The assignment ptr1 = ptr2 let ptr1 and ptr2 point to the same object and increments a counterthat is shared between all pointer instances that references this object. The destruction of onepointer or explicit reset() on that pointer decrements the shared counter (or use count). If theshared counter drops to zero and the data will be deleted. Note that releasing a reference countingsmart pointer is not possible.

Example: Useful in every day code where one object is needed in multiple places and no place cantell when to delete the object. It’s common to use reference counting pointers in GUI applications.

Reference linking pointer

Reference linking is similiar to reference counting. But instead of a counter a (typically circular)doubly linked list of all smart pointers that point to the same object is maintained. Then theassignment ptr1 = ptr2 adds ptr1 to the list of smart pointers and the size of the list increasesby one. The destruction of one pointer or explicit reset() removes one element from the list andthe size of the list decreases by one. If the size of the list drops to zero the data will be deleted.

Example: Reference linking should be used instead of reference counting if all other pointers forone object should be notified or if the object should be able to be released.

Copy on write (COW) pointer

The copy on write pointer, or COW pointer for short, is the most complex one in this list ofstrategies. It uses a mixture of reference counting or reference linking and object copying like thecopied pointer does.

The COW pointer uses reference counting or linking as long as the pointed object is not modified.When it is about to be modified, that is if the first non-const member function is called, the objectis copied and only the copy gets modified.

Example: Imagine a template image that can be requested inside a COW pointer. As long as theimage does not get modified, only references are passed around, which saves time and memory. Ifthe image is about to be modified, the COW pointer copies the image and the template image isguaranteed not to be modified.

Page 11: smart pointer

4 REFERENCE COUNTING SMART POINTERS 10

4 Reference counting smart pointers

Reference counting is a powerful and often used strategy. In the first part of this section areference counting implementation will be introduced. In the second part some issues that ariseare discussed.

4.1 Reference counting implementation

The target of the section is a simplyfied lightweight version of a reference counting smart pointer.There are two things that are shared between the instances of smart pointers that reference thesame data: The data pointer and the reference counter for that data. That is because incre-mentation or decrementation of the counter in one pointer must be visible in all pointers for theappropriate data.

Figure 1: Smart pointer structure

To indicate a pointer with no target (a null pointer) the shared counter pointer and data pointerare set to null. There are only two possible cases: Data and counter pointer are both null or bothnot null.

Figure 2: Null smart pointer

increment() and decrement()

The following member functions define code for incrementing and decrementing the shared counter.The member count is of type int*.

void ref_ptr::increment (void)

{

if (ptr == 0) return;

++(*count);

}

void ref_ptr::decrement (void)

{

if (ptr == 0) return;

if (--(*count) == 0)

{

delete count;

delete ptr;

}

}

Page 12: smart pointer

4 REFERENCE COUNTING SMART POINTERS 11

Constructors and destructor

The constructors initialize smart pointer instances. The following constructor is for creating a nullsmart pointer. For every null smart pointer the following holds: ptr == 0 and count == 0.

Note that count == 0 does not mean that the reference counter is set to 0 but count is a nullpointer.

// Init data and count pointer with null

ref_ptr::ref_ptr (void) : ptr(0), count(0)

{

}

To initialize the smart pointer from a given dump pointer a new counter needs to be established.

// Init pointer with new data and create new counter

explicit ref_ptr::ref_ptr (T* p) : ptr(p)

{

count = (p == 0) ? 0 : new int(1);

}

To initialize the smart pointer from another smart pointer the data and counter pointer need tobe copied and the counter needs to be incremented.

// Init data and counter pointer from source, increment

ref_ptr::ref_ptr (const ref_ptr<T>& src) : ptr(src.ptr), count(src.count)

{

increment()

}

The destructor just decrements the counter. If the counter drops to zero, data and count will bedeleted within the decrement() member function.

// Decrement the shared reference counter

ref_ptr::~ref_ptr (void)

{

decrement();

}

Dereference operators

There are two dereference operators. The first one is for accessing the dereferenced data, thesecond one is the direct member access or pointer to member operator which is used for callingmember functions or accessing member variables.

// Dereference operator

T& ref_ptr::operator* (void) const

{

return *ptr;

}

// Member access operator

T* ref_ptr::operator-> (void) const

{

return ptr;

}

Page 13: smart pointer

4 REFERENCE COUNTING SMART POINTERS 12

Assignment operators

The assignment operators are the most complicated ones. The following objectives need to beaccomplished:

• The old reference needs to be decremented.

• The old data and counter need to be destroyed if the counter drops to zero.

• The new data and reference counter need to be assigned.

• The new reference counter needs to be incremented.

But the resulting code is relatively simple.

// Assignment operator from shared pointer

ref_ptr<T>& ref_ptr::operator= (const ref_ptr<T>& rhs)

{

if (ptr == rhs.ptr) return;

decrement();

ptr = rhs.ptr;

count = rhs.count;

increment();

return *this;

}

Note that the comparison between ptr and src.ptr is essential! Imagine the following code:

ref_ptr<MyObject> pointer(new MyObject);

pointer = pointer;

Without this comparision the above code would lead to an error (typically segmentation faulton unix machines). Some STL algorithms make such assignments while sorting a container, forexample std::sort. This comparison could be left out if src is passed as copy and not asreference, but performance would be worse.

An alternative, which relies on the constructor and destructor implementation, for the memberfunction above would be:

// Assignment operator from shared pointer

ref_ptr<T>& ref_ptr::operator= (const ref_ptr<T>& rhs)

{

ref_ptr<T> tmp(rhs);

swap(tmp);

return *this;

}

The assignment from a dump pointer works similar:

// Assignment operator from pointer

ref_ptr<T>& ref_ptr::operator= (T* rhs)

{

decrement();

ptr = rhs;

count = (ptr == 0) ? 0 : new int(1);

increment();

return *this;

}

Page 14: smart pointer

4 REFERENCE COUNTING SMART POINTERS 13

Comparison operators

Comparison can only be implemented for equality and unequality. The standard does not definethe meaning of ”one pointer is less than another” (unless both pointers point into the same array),so a member operator< or something similar is not supported.

// Comparison with smart pointer

bool ref_ptr::operator== (ref_ptr<T>& p) const

{

return ptr == p.ptr;

}

// Comparison with pointer

bool ref_ptr::operator== (T* p) const

{

return ptr == p;

}

The implementations for operator!= are straight forward with the == replaced by != and left outhere.

Other member functions

Some more member functions need to be defined. The first one is to reset the smart pointerand to make it a null pointer. Resetting the smart pointer is equivalent to pointer = 0;. Notethat releasing a reference counting smart pointer from beeing managed is not possible withoutadditional book keeping and not implemented here.

void ref_ptr::reset (void)

{

decrement();

ptr = 0; count = 0;

}

The second member function is for simple needs: It just returns the data pointer. This is equivalentto &(*pointer) but only if operator& is not overloaded for the managed data type. Therefore,using &(*pointer) is not recommended.

T* ref_ptr::get (void)

{

return ptr;

}

Swapping two smart pointers with a separate swap() member function instead of using code liketmp = a; a = b; b = a; is much more efficient as the performace analysis will demonstrate. Seeperformance analysis at section 4.2.3 for details.

void swap (ref_ptr<T>& p)

{

T* tp = p.ptr; p.ptr = ptr; ptr = tp;

int *tc = p.count; p.count = count; count = tc;

}

To receive information about the reference counter the following member function can be used.The use count is always greater than zero except for a null pointer.

int ref_ptr::use_count (void)

{

return (count == 0) ? 0 : *count;

}

Page 15: smart pointer

4 REFERENCE COUNTING SMART POINTERS 14

Down- and upcasting

The reference counting smart pointer is now ready to use for simple every day code. But somesemantics has not yet been modeled. The following hierarchy of classes is the basis for an exampleto illustrate that the smart pointer does not yet have the desired semantics.

class Base { ... };

class Sub : public Base { ... };

It is always possible to store a pointer with a subclass type to a pointer variable of a base classtype using bare pointer arithmetic. Thus this is valid C++ code:

Sub* p1 = new Sub;

Base* p2;

p2 = p1; // Will compile, pointers are polymorphic

An equivalent code with dump pointers replaced by smart pointers should be possible, too. Butthere is a problem:

ref_ptr<Sub> p1(new Sub);

ref_ptr<Base> p2;

p2 = p1; // Will not compile, no operator= for that type

The problem is a missing assignment operator that takes a right hand side from a different dumpor smart pointer type. Since this type is not known in advance member templates must be used.Introducing the following new template member function is a solution that allows assignments fromother right hand sides Y instread of T (where Y can be any type but T is always the parametertype for instances of this class).

// Assignment operator from different shared pointer

template <class Y>

ref_ptr<T>& ref_ptr::operator= (const ref_ptr<Y>& rhs)

{

if (ptr == rhs.ptr) return;

decrement();

ptr = rhs.ptr;

count = rhs.count;

increment();

return *this;

}

This codes leads to another problem. Since rhs is of type ref ptr<Y> and associated objects fromthis class are of type ref ptr<T>, there is no access to rhs.ptr and rhs.count because the ptr

and count members are private and can only be accessed by objects of the same type.

Two approaches will help out. The first one is to make the appropriate members public. Thesecond and much better approach is to introduce template friends that allow access from allref ptr types. In the reverse direction the type ref ptr<Y> will grant access from all ref ptr

types.

To also support all operations from other smart pointer types, all constructors, assignment andcomparison operators need to be reintoduced with a template parameter. And since some compilersare not capable of member templates or template friends, it should be possible to easily disablethe new code. The following outline declares the new members, the definition is identical to thedefinitions of the appropriate non-template member functions.

Page 16: smart pointer

4 REFERENCE COUNTING SMART POINTERS 15

template <class T>

class ref ptr

{

// Old code here

#ifndef NO MEMBER TEMPLATES

/* Template friends for accessing different ref ptr’s. */

private:

template <class Y> friend class ref ptr;

/* Template members for other ref ptr types. */

public:

template <class Y> explicit ref ptr (Y* p);

template <class Y> ref ptr (const ref ptr<Y>& src);

template <class Y> ref ptr<T>& operator= (const ref ptr<Y>& rhs);

template <class Y> ref ptr<T>& operator= (Y* rhs);

template <class Y> bool operator== (const ref ptr<Y>& p);

template <class Y> bool operator!= (const ref ptr<Y>& p);

#endif

};

4.2 Reference counting issues

Like the problem with up- and downcasting, which applies to all smart pointers, there are issuesspecific to reference counting. One of the biggest problems are cyclic references. The next sectionevince the situation and a way to fix most of the occurences.

4.2.1 Cyclic references

Cyclic (or circular) references are a significant limitation of the reference counting strategy. Ifa set of smart pointers loses all references from the outside, it becomes garbage and should bereclaimed. But if the set contains one or more cyclic references, the reference counters never dropsto zero and these cycles are not reclaimed.

Example 1: A cyclic singly liked list: A singly linked list serves as an example. A listconsists of items and an interface that manages these items. Destroying the list interface shoulddestroy all the items like the STL containers used to do.

Figure 3: Ordinary list structure

Destruction of all the list items happens because destroying the interface decrements the referencecounter of the first list item to zero. So the first list item gets destroyed, which decrements therecerence counter of the second item to zero, and so on.

Page 17: smart pointer

4 REFERENCE COUNTING SMART POINTERS 16

Figure 4: Reclaimed, ordinary list structure

The problem arises if there is any circle within the references. For illustration purposes the lastlist item references the first item now. Note that the first list item has a reference count of two,because it is referenced by the interface and the last list item.

Figure 5: Cyclic list structure

The destruction of the interface decrements the reference counter of the first list item to oneinstead of zero. This means that the list items are not reclaimed because none of the referencecounters is zero, and all items that are part of the cycle leak.

Figure 6: Reclaimed, cyclic list structure

Example 2: A graph data structure: To model a graph data structure one needs to introduceclasses for nodes and for edges. To keep it simple, the following minimal code should be enough:

class Node, Edge;

typedef ref_ptr<Node> NodePtr;

typedef ref_ptr<Edge> EdgePtr;

class Node

{

std::list<EdgePtr> edges;

};

class Edge

{

NodePtr predecessor;

NodePtr successor;

};

Page 18: smart pointer

4 REFERENCE COUNTING SMART POINTERS 17

Every node can have multiple edges and every edge references two nodes, a predecessor and asuccessor node. This makes the references cyclic, because a node references one or more edges,and each of the edges references the node. The illustration visualizes the cycle.

Figure 7: Graph structure: UML-like notation

The question is how to fix these cycles or how to handle cyclic references. Some libraries offer asolution for that problem.

Weak pointers: A new pointer type called ”weak pointer” is introduced. The already intro-duced ref ptr can be treated as a ”strong pointer”. The idea is to let both, the strong and theweak pointers, share a data object with some limitations to the weak pointer: The weak pointerstores a ”weak reference” to an object that is already managed by a strong pointer. To accessthe object, a weak pointer can be converted to a strong pointer. This is typically done by callingthe lock() member function or using an appropriate constructor from the strong pointer. Whenthe last strong pointer to the object goes away and the object is deleted, the attempt to obtain astrong pointer from the weak pointer instances that refer to the deleted object will fail.

The implementation of weak pointers will not go into detail but the steps are relatively easy. Thefirst thing that needs to be changed is the shared counter. The counter now needs to keep trackof strong and weak references to the data. A single integer for the counter is insufficient and weneed to provide a counter class:

class Count

{

public:

int ref_cnt;

int weak_cnt;

};

The counter object must not be destroyed until all references (weak and strong ones) have dis-appeared, because the weak pointers need to check if the data is still available and has not beenreclaimed (and that is, if ref cnt > 0).

Figure 8: Weak and strong pointers

Rules for destroying the data and the counter look like this:

• The data need to be reclaimed if ref cnt == 0.

• The counter need to be destroyed if weak cnd == 0 AND ref cnt == 0.

Page 19: smart pointer

4 REFERENCE COUNTING SMART POINTERS 18

Example 2: A graph data structure (Continued): The cycles in the graph data structurecan easily be fixed with replacing strong node references by weak references. The resulting codelooks like this:

class Node, Edge;

typedef weak_ptr<Node> WeakNodePtr;

typedef ref_ptr<Edge> EdgePtr;

class Node

{

std::list<EdgePtr> edges;

};

class Edge

{

WeakNodePtr predecessor;

WeakNodePtr successor;

};

There are no more cycles in the structure as the new visualization shows. Converting the weakpointers to strong pointers in the edge will never fail because edges can semantically not existwithout the predecessor and the successor node.

Figure 9: New graph structure: UML-like notation

4.2.2 Multi-Threading

Multi-Threading is always a problematic issue. Smart pointers suffer from some inconsistencies inmulti-threaded environments, and multi-threading issues ultimately affect smart pointers’ imple-mentation. The introduced reference counting smart pointer is not thread safe because the criticaloperations are not atomic.

If smart pointers are copied between threads, incrementing and decrementing the reference counterhappens at unpredictable times. And the most critical operation is decrementing the counter(because incrementing never frees memory).

The following piece of code is used to illustrate the problem (it is the decrement member functionin a slightly different version):

void ref_ptr::decrement (void)

{

if (ptr == 0) return;

*count -= 1;

if (*count == 0)

{

delete count;

delete ptr;

}

}

Page 20: smart pointer

4 REFERENCE COUNTING SMART POINTERS 19

The problem arises if two threads execute the decrement member function simultanously. A simpleexample of unintended behaviour could happen like this: Two threads have each a smart pointerto the same data, thus the reference counter has two as value.

The first thread executes the statements until and including *count -= 1;. Then this thread ispaused but the use count is already decremented to one. The other thread decrements the counterto zero and deletes the counter and data afterwards. If the first thread continues execution, thedata and counter are about to be deleted the second time and the program will crash.

A simple solution for that problem is using a mutex to create an area with mutally exclusion ofprogram flow. This makes the decrement() member function atomic.

void ref_ptr::decrement (void)

{

if (ptr == 0) return;

mutex.lock();

if (--(*count) == 0)

{

delete count;

delete ptr;

}

mutex.unlock();

}

4.2.3 Performance analysis

The use of smart pointers buys a lot. But it also has its costs. Using smart pointers has perfor-mance (CPU time) and memory penalties.

Memory penalties: Every reference counting smart pointer instance takes up 2·sizeof(void*)bytes on the stack (8 bytes an 32 bit machines). This size consists of the size for the data pointerand the counter pointer. A bare pointer only takes up sizeof(void*) bytes, which is half thesize. In the heap the smart pointer allocates additional memory of sizeof(Count) bytes whereCount is either just an int or a counter class.

But performance is not only a matter of a few bytes of memory. The time to allocate this memoryis significant.

Performance penalties: The process of allocating memory (which is the process of requestingmemory from the operating system) is expensive. There is almost no difference between reqesting4 or 4000 bytes, but there is a huge difference between allocating memory one or two times. Thusthe number of allocations is significant, not the size of one allocation.

Page 21: smart pointer

4 REFERENCE COUNTING SMART POINTERS 20

Allocation and deletion test

Since the reference counting smart pointer has a data and a counter pointer, two instead of oneallocations are needed. The following diagrams compare these items with one another:

• A bare pointer (denoted with a red line),

• The introduced ref ptr (denoted with a green line),

• The Boost shared ptr (denoted with a blue line),

• The Boost shared ptr with multi-threading support (denoted with a purple line).

The values along the x-axis are the amount of allocations and deletions while the values along they-axis are the chronometries for these allocations and deletions in seconds.

Left diagram: The data object that has beenused for this test was the primitive type int

which has a size of 4 bytes. The counter alsohas a size of 4 bytes. Thus to create a referencecounting smart pointer two allocations need tobe done. For this reason the ref ptr needsabout twice the time than the dump pointer,which only needs to allocate 4 bytes for the data.

Right diagram: This diagram was createdusing a data object with a size of 4 megabytes(one million times bigger). The difference be-tween the dump pointer and the smart pointersis almost zero because the additional counter al-location takes a proportionally short time.

Figure 10: Alloc and del, 4 byte data Figure 11: Alloc and del, 4 MB data

Most of the objects are typically small and objects with a size of one megabyte and up areunrealistic. However, 3’500’000 allocations can be done within one second (instead of 7’000’000with a dump pointer). This should be enough for most of the cases and stable programms aremore important than slightly more CPU time.

Page 22: smart pointer

4 REFERENCE COUNTING SMART POINTERS 21

Copy operation time

Copying a smart pointer using an assignment like p1 = p2 is more expensive than copying a dumppointer. That is because the counter of p1 need to be decremented (and possibly deleted), thenthe data and counter pointer need to be copied and the new counter need to incremented.

The following diagrams use the same notation as the previous diagrams. The values along thex-axis are the amount of swap operations while the values along the y-axis are the chronometriesfor these operations in seconds.

Left diagram: The left diagram compares thetime that is used to swap smart pointer variablesmanually with a third temporary variable.

The dump pointer nearly uses zero time forthis test compared to the smart pointers. TheBoost smart pointer with multi-threading sup-port is far away from having competitive tim-ings. Thats because making increment anddecrement atomic operations is expensive.

Right diagram: This diagram visualizes tim-ings for code that uses the swap() member func-tion instead of swapping manually with a tem-porary variable.

Note that the values of the x-axis from the pre-vious test are in range from 0 to 107. The newdiagram’s range reaches from 0 to 108 which isten times greater. The old range is yellow col-ored for comparison reasons.

Figure 12: Copy operation, manual swapping Figure 13: Copy operation, smart swapping

It is easy to read off that the Boost pointer now needs about 0.4 seconds instead of 3.5 secondsfor the same amount of manual swapping operations. Both, the Boost pointer with and withoutmulti-threading support have equal timings now, which means that Boost undertakes no effort inatomizing the swap() operation.

If swapping of smart pointers is an issue, it is strongly recommended to use the swap() mem-ber function. This member function just swaps the data and counter pointers and circumventsincrementing and decrementing the counters of the participating smart pointers.

Page 23: smart pointer

5 SMART POINTER LIBRARIES 22

5 Smart pointer libraries

Most smart pointer libraries implement standard strategies which are not discussed here. Thesedescriptions of strategies can be found at section 3.4 above.

5.1 The C++ Standard Library

The C++ Standard Library comes with just one simple smart pointer called auto ptr. In futurereleases of the Standard Library all boost smart pointers will be included. See

http://www.open-std.org/jtc1/sc22/WG21/

for detailed information about TR (technical report) and ”General Purpose Smart Pointers”.

The auto ptr is a smart pointer with ownership strategy. This strategy passes responsibility forreclaiming the data or, if responsibility has not been passed, reclaims the data if the auto ptr

leaves scope or reset() is called.

5.2 The Boost Library

The Boost Library comes with several smart pointers which will be part of future versions of theC++ Standard Library.

scoped ptr: The scoped pointer is a lightweight and simple pointer for sole ownership of singleobjects.

The scoped pointer stares a pointer to a dynamically allocated object, which is reclaimed either ondestuction of the smart pointer on via explicit reset(). It has no shared ownership or ownershiptranfer semantics and is non-copyable. The scoped pointer is safer and faster for objects thatshould not be copied.

The scoped pointer does not meet the CopyConstructible and Assignable requirements for STLcontainers. There is also a version called scoped array which is for sole ownership of arrays.

shared ptr: The shared pointer is an external reference counting pointer with ownership sharedamong multiple pointers.

The shared pointer does meet the requirements for STL containers. There is also a version calledshared array which is for array ownership among muliple pointers.

weak ptr: The weak pointer is a non-owning observer of a shared ptr-owned object. There isno need for an appropriate weak array variant because weak pointers never reclaim the data.

The weak pointer does meet the requirements for STL containers.

intrusive ptr: The intrusive pointer is a shared ownership pointer (like shared pointer) withembedded reference count. It is a lightweight shared pointer for objects which already have internalreference counting. Thus, the intrusive pointer is smaller and faster than the shared pointer.increment() and decrement() operations are forwarded to user defined member functions.

The intrusive pointer does meet requirements for STL containers.

5.3 The Loki Library

The Loki Library has several smart pointers which are described in the book ”Modern C++Design” from Andrei Alexandrescu. The Loki Library offers the following smart pointer types:

Page 24: smart pointer

5 SMART POINTER LIBRARIES 23

RefCounted: This pointer supports classic external reference counting like Boost’s shared ptr.There is no support for weak pointers and it is not thread-safe. This pointer meets the requirementsfor STL containers.

RefCountedMTAdj: This pointer is similar to RefCounted but it has support for multi-threadedenvironments. It can of course also be stored in STL containers.

DeepCopy: This pointer implements deep copy semantics. It is similar to the copied pointerstrategy, but leaves the deep copy implementation to the user. For this purpose it assumes aClone() member function. The copied pointer strategy only implements flat copying. Since deepcopying satisfies appropriate container requirements, a pointer of this type can be stored in STLcontainers.

RefLinked: This smart pointer implements the referene linking strategy. Reference linking issimilar to reference counting, thus this pointer can be stored in STL containers.

Destructive Copy: The destructive copy smart pointer implements the ownership transfer strat-egy just like std::auto ptr. It is not possible to store this pointer in STL containers.

NoCopy: This smart pointer implements the scoped pointer strategy like boost::scoped ptr.This smart pointer cannot be copied, thus there is no way to get it in and out of STL containers.

Page 25: smart pointer

A REF PTR SOURCE CODE 24

A ref ptr source code

/*

* A reference counting smart pointer for C++

* Copyright (c) 2005 by Simon Fuhrmann

*

* This code is free software; you can redistribute it and/or modify

* it under the terms of the GNU General Public License as published by

* the Free Software Foundation; version 2 dated June, 1991.

*

* This program is distributed in the hope that it will be useful,

* but WITHOUT ANY WARRANTY; without even the implied warranty of

* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

* GNU General Public License for more details.

*

* On Debian GNU/Linux systems, the complete text of the GNU General

* Public License can be found in ‘/usr/share/common-licenses/GPL’.

*

* A copy of the GNU General Public License is also available at

* <URL:http://www.gnu.org/copyleft/gpl.html>.

*/

#ifndef REF PTR HEADER

#define REF PTR HEADER

template <class T>

class ref ptr

{

/* Private declaration of class members. */

private:

T* ptr;

int* count;

/* Private definition of member methods. */

private:

void increment (void)

{

if (ptr == 0) return;

++(*count);

}

void decrement (void)

{

if (ptr == 0) return;

if (--(*count) == 0)

{

delete count;

delete ptr;

}

}

Page 26: smart pointer

A REF PTR SOURCE CODE 25

/* Public definition of member methods. */

public:

/* Ctor: Default one. */

ref ptr (void) : ptr(0), count(0)

{ }

/* Ctor: From pointer. */

explicit ref ptr (T* p) : ptr(p)

{ count = (p == 0) ? 0 : new int(1); }

/* Ctor: Copy from other ref ptr. */

ref ptr (const ref ptr<T>& src) : ptr(src.ptr), count(src.count)

{ increment(); }

/* Destructor. */

~ref ptr (void)

{ decrement(); }

/* Assignment: From other ref ptr. */

ref ptr<T>& operator= (const ref ptr<T>& rhs)

{

if (rhs.ptr == ptr) return *this;

decrement();

ptr = rhs.ptr;

count = rhs.count;

increment();

return *this;

}

/* Assignment: From pointer. */

ref ptr<T>& operator= (T* rhs)

{

if (rhs == ptr) return *this;

decrement();

ptr = rhs;

count = (ptr == 0) ? 0 : new int(1);

return *this;

}

/* Operations. */

void reset (void)

{

decrement();

ptr = 0; count = 0;

}

void swap (ref ptr<T>& p)

{

T* tp = p.ptr; p.ptr = ptr; ptr = tp;

int *tc = p.count; p.count = count; count = tc;

}

Page 27: smart pointer

A REF PTR SOURCE CODE 26

/* Dereference. */

T& operator* (void) const

{ return *ptr; }

T* operator-> (void) const

{ return ptr; }

/* Comparison. */

bool operator== (const T* p) const

{ return p == ptr; }

bool operator== (const ref ptr<T>& p) const

{ return p.ptr == ptr; }

bool operator!= (const T* p) const

{ return p != ptr; }

bool operator!= (const ref ptr<T>& p) const

{ return p.ptr != ptr; }

bool operator< (const ref ptr<T>& rhs)

{ return ptr < rhs.ptr; }

/* Information. */

int use count (void) const

{ return (count == 0) ? 0 : *count; }

T* get (void) const

{ return ptr; }

#ifndef NO MEMBER TEMPLATES

/* Template friends for accessing diffrent ref ptr’s. */

private:

template <class Y> friend class ref ptr;

public:

/* Ctor: From diffrent pointer. */

template <class Y>

explicit ref ptr (Y* p) : ptr(p)

{ count = (p == 0) ? 0 : new int(1); }

/* Ctor: Copy from diffrent ref ptr. */

template <class Y>

ref ptr (const ref ptr<Y>& src) : ptr(src.ptr), count(src.count)

{ increment(); }

Page 28: smart pointer

A REF PTR SOURCE CODE 27

/* Assignment: From diffrent ref ptr. */

template <class Y>

ref ptr<T>& operator= (const ref ptr<Y>& rhs)

{

if (rhs.ptr == ptr) return *this;

decrement();

ptr = rhs.ptr;

count = rhs.count;

increment();

return *this;

}

/* Assignment: From diffrent pointer. */

template <class Y>

ref ptr<T>& operator= (Y* rhs)

{

if (rhs == ptr) return *this;

decrement();

ptr = rhs;

count = (ptr == 0) ? 0 : new int(1);

return *this;

}

/* Comparison with diffrent ref ptr type. */

template <class Y>

bool operator== (const ref ptr<Y>& p) const

{ return p.ptr == ptr; }

template <class Y>

bool operator!= (const ref ptr<Y>& p) const

{ return p.ptr != ptr; }

template <class Y>

bool operator< (const ref ptr<Y>& rhs) const

{ return ptr < rhs.ptr; }

#endif /* NO MEMBER TEMPLATES */

};

#endif /* REF PTR HEADER */

Page 29: smart pointer

LIST OF FIGURES 28

List of Figures

1 Smart pointer structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102 Null smart pointer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 Ordinary list structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154 Reclaimed, ordinary list structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165 Cyclic list structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166 Reclaimed, cyclic list structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167 Graph structure: UML-like notation . . . . . . . . . . . . . . . . . . . . . . . . . . 178 Weak and strong pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179 New graph structure: UML-like notation . . . . . . . . . . . . . . . . . . . . . . . . 1810 Alloc and del, 4 byte data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2011 Alloc and del, 4 MB data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2012 Copy operation, manual swapping . . . . . . . . . . . . . . . . . . . . . . . . . . . 2113 Copy operation, smart swapping . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21


Top Related