13.7. An Introduction To Smart Pointers

Time: 00:04:23 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PowerPoint)
Review

C programmers have long acknowledged the challenge of using pointers. Creating reusable data-structure libraries further compounds the challenges. Void pointers can generalize the library functions but require the client to typecast the pointers to a "known" or defined type before using them. Furthermore, the client is responsible for initializing the pointers and deallocating the structure's heap memory. While the C libraries typically provide functions completing these tasks, application programmers can easily overlook calling them. C++'s template variables solve the generalization problem without requiring casts, and its constructors and destructors alleviate the initialization and deallocation problems. Nevertheless, pointer errors remain a persistent source of program bugs. Smart pointers address many of the remaining bugs.

Smart pointers wrap traditional or raw pointers with template classes, further reducing the complexity leading to their associated bugs. They provide an efficient kind of automatic garbage collection with minimal overhead. (Smart pointers still manage memory deallocation with destructors rather than running a program-wide mark and sweep algorithm.) However, automatic memory management is only one smart-pointer advantage: smart pointers make the wrapped raw pointer private and only allow access through a limited public interface. Raw pointers are a frequent source of runtime errors. In conjunction with C++'s stronger type checking and smart pointer's controlled interface, the compiler can detect many errors before program execution, simplifying and hastening their detection and correction.

The Primary Smart Pointer Components

Programmers access the smart pointer system with the <memory> header file, which contains the smart pointer class specifications and function prototypes. Although the system is extensive, three smart pointer classes and a few of their member functions provide the fundamental and most frequently used features.

  unique_ptr<T> shared_ptr<T> weak_ptr<T>
  A pointer exclusively responsible for deallocating a resource. A pointer with shared responsibility for deallocating a resource. A shared pointer that does not add to a pointer's reference count.
Create make_unique<T>() make_shared<T>()  
lock()    
operator->  
operator*  
operator bool()  
operator==
operator!=
   
expired()    
use_count()  
get()  
reset()
release()    
unique()    
Smart pointer summary. Smart pointers manage resources allocated on the heap. They maintain a count of the number of smart pointers pointing to a resource (i.e., a reference count) and deallocate it when the count becomes zero. This figure summarizes the three smart pointer classes and their most frequently used functions, while the following figures provide simple programming examples. The online <memory> documentation provides additional detail, making it a helpful reference.

Typical Smart Pointers

class part
{
    private:
        string name;

    public:
        part(string n) : name(n) {}
        ~part() { cout << "dtor\n"; }
};
int main()
{
    shared_ptr<part> p1 =
        make_shared<part>("Widget");
    shared_ptr<part> p2 = p1;
    weak_ptr<part> w = p1;
    cout << p1.use_count() << endl;
    return 0;
}
Basic smart pointer behavior. p1 and p2 are shared pointers pointing to a resource, an instance of the part class, and w is a weak pointer, sharing the reference count without increasing it. Destroying a shared pointer reduces the reference count by one, but the program only destroys or deallocates the resource when the count becomes zero. In this simple example, the program automatically destroys the pointers when it ends, and when destroyed, the last shared pointer calls the part destructor, printing the "dtor" message. The program prints two lines:
2
dtor

Smart Pointer Examples

The text demonstrates the smart pointer syntax and behavior with small, simple programs. The examples all depend on the same library classes, implying that they require the same header files. Furthermore, each replaces the template variable with the same part class. So, for brevity, the following figure presents these common features.

#include <iostream>
#include <string>
#include <memory>
using namespace std;

class part
{
	private:
		string	name;
		int	id;

	public:
		part(string n, int i) : name(n), id(i) {}
		string get_name() { return name; }
};
Components in common with all smart pointer examples.

Unique Smart Pointer Example

void f1(unique_ptr<part>& p)				// (a)
{
	cout << p->get_name() << endl;
}

int main()
{
	unique_ptr<part> unique				// (b)
		= make_unique<part>("Widget", 10);	// (c)

	cout << unique->get_name() << endl;		// (d), prints '(1) Widget'
	f1(unique);					// prints '(2) Widget'

	part* p = unique.release();			// (e)
	cout << p->get_name() << endl;			// (f), prints '(3) Widget'

	if (unique)					// (g)
		cout << unique->get_name() << endl;	// No output
	else
		cout << "unique is empty\n";		// prints 'unique is empty'

	unique.reset(new part("Screw", 30));		// (h)

	if (unique)
		cout << unique->get_name() << endl;	// prints '(4) Screw'
	else
		cout << "unique is empty\n";		// No output

	return 0;
}
Creating and using a unique_ptr. Unique pointers provide client programs with a single, exclusive access point to dynamic resources. They maintain exclusion by allowing access through a controlled public interface that restricts the behavior of the assignment operator.
  1. Programs can pass unique pointers by reference (creates an alias) but not by value (copies an object).
  2. Defines a unique pointer managing a part pointer.
  3. Makes a unique_ptr, implicitly calling new to create the part object.
  4. The overloaded arrow operator accesses the managed part object, but doesn't allow address arithmetic.
  5. The release removes and returns the managed raw pointer, leaving the unique pointer, unique, empty.
  6. p is a raw pointer pointing to the original part object, making the arrow the "normal" address operator.
  7. The expression (unique) invokes operator bool(), returning true if the unique pointer is not empty, or false if it is.
  8. If the unique pointer is not empty, reset destroys the managed object. The argument becomes the unique pointer's new managed object.

Shared Smart Pointer Example

//void f2(shared_ptr<part>& p)								// (a)
void f2(shared_ptr<part> p)
{
	cout << "(4) " << p->get_name() << "  " << p.use_count() << endl;
}

int main()
{
	shared_ptr<part> shared =							// (b)
		make_shared<part>("Bolt", 20);						// (c)
	shared_ptr<part> shared2 = shared;						// (d)
	shared_ptr<part> shared3 = make_shared<part>("Bolt", 20);			// (e)

	cout << "(1) " << shared->get_name() << "  " << shared.use_count() << endl;	// (f), prints '(1) Bolt  2'
	cout << "(2) " << shared2->get_name() << "  " << shared2.use_count() << endl;	// (f), prints '(2) Bolt  2'
	cout << "(3) " << shared3->get_name() << "  " << shared3.use_count() << endl;	// (g), prints '(3) Bolt  1'

	f2(shared);									// (h), prints '(4) Bolt  3'

	if (shared.unique())								// (i)
		cout << "Unique\n";							// No output
	else
		cout << "Shared\n";							// prints 'Shared'

	shared.reset(new part("Screw", 30));						// (j)

	if (shared)									// (k)
		cout << shared->get_name() << endl;					// (l), prints 'Screw'
	else
		cout << "shared is empty\n";						// No output

	return 0;
}
Creating and using shared_ptr objects. Multiple shared pointers can point to the same object or resource. Each pointer maintains a count of the number of shared pointers pointing to the object and only destroys the object when the count goes to 0.
  1. Programs can pass shared pointers by reference or value.
  2. Defines a shared pointer managing a part pointer.
  3. Makes a shared pointer, implicitly calling new to create the part object.
  4. Copies shared to shared2, increasing the reference count to 2 for both shared pointers.
  5. Makes a new shared pointer. Although the contents of the managed object are identical to shared, the implicit call to new returns a distinct part object. Therefore, shared3 has a reference count of 1, and the counts for shared and shared2 are unchanged.
  6. shared and shared2 manage the same part object.
  7. shared3 manages a different object than the other pointers.
  8. While f2 runs, three shared pointers, shared, shared2, and p, point to the same part object, resulting a reference count of 3.
  9. If shared is the only shared pointer pointing to an object, unique returns true; otherwise it returns false. shared.unique() is equivalent to shared.use_count == 1
  10. If the shared pointer is not null (as it is in this example), reset destroys the managed object. The shared pointer acquires the argument as its newly managed object with a count of 1.
  11. (shared) invokes operator bool(), returning true if the shared pointer is not empty, or false if it is.
  12. -> is the overloaded smart pointer operator.

Weak Smart Pointer Example

void f3(shared_ptr<part> p1, weak_ptr<part> p2)						// (a)
{
	cout << "(3) " << p1.use_count() << " " << p2.use_count() << endl;		// (b), prints '(3) 2 2'
}

int main()
{
	shared_ptr<part> shared = make_shared<part>("Gadget", 40);
	weak_ptr<part> weak = shared;							// (c)

	cout << "(1) " << shared->get_name() << " " << shared.use_count() << endl;	// prints '(1) Gadget 1'
	cout << "(2) " << weak.use_count() << endl;					// (d), prints '(2) 1'

	f3(shared, weak);

	shared_ptr<part> locked = weak.lock();						// (e)
	cout << "(4) " << shared.use_count() << "  " << locked.use_count()		// (f), prints '(4) 2  2  2'
		<< "  " << weak.use_count() << endl;

	weak.reset();									// (g)
	if (weak.expired())								// (h)
		cout << "weak unavailable" << endl;					// prints
	else
		cout << weak.use_count() << endl;					// no output
	cout << "(5) " << shared.use_count() << "  " << locked.use_count() << endl;	// (i), prints '(5) 2  2'

	locked.reset();									// (j)
	cout << "(6) " << shared.use_count() << "  " << locked.use_count() << endl;	// (k), prints '(6) 1  0'

	if (locked)									// (l)
		cout << locked.use_count() << endl;					// no output
	else
		cout << "locked unavailable" << endl;					// prints
	cout << "(7) " << shared.use_count() << endl;					// (m), prints '(7) 1'

	return 0;
}
Making and using a weak_ptr objects. Weak pointers are restricted shared pointers that point to a resource without the responsibility for destroying it, so they don't increase the reference count. Furthermore, they do not override operator-> or operator*, meaning they can't directly access the resource.
  1. Programs can pass weak pointers by value or reference.
  2. The new pointers p1 and p2 share the same resource as shared (created in main), but only p1 increases the reference count.
  3. Programs copy a shared to a weak pointer.
  4. weak shares a resource with shared but doesn't increase the reference count.
  5. Programs access a weak pointer's resource by locking it, creating a shared pointer. The program can't destroy the weak pointer's resource while it's locked.
  6. Three pointers share a resource, but only two affect the reference count.
  7. Resetting weak expires or empties it.
  8. expired() is true if the pointer is empty.
  9. shared and locked are still active.
  10. Resetting locked empties it, reducing the reference count.
  11. shared still refers to the resource, but not locked.
  12. operator bool() is false because lock is empty.
  13. shared is active and counted.

Smart Pointer Example Code

ViewDownloadComments
basic.cpp basic.cpp A simple smart pointer example, including the #include directives
unique.cpp unique.cpp The complete unique smart pointer example
shared.cpp shared.cpp The complete shared smart pointer example
weak.cpp weak.cpp The complete weak smart pointer example