Home > Articles > Programming > C/C++

C++ Reference Guide

Hosted by

Toggle Open Guide Table of ContentsGuide Contents

Close Table of ContentsGuide Contents

Close Table of Contents

Using unique_ptr, Part II

Last updated Jan 1, 2003.

If youโ€™re looking for more up-to-date information on this topic, please visit our C/C++ Programming article, podcast, and store pages.

In the last part of this series I will explain why unique_ptr usage in generic containers and algorithms is safe, show how to implement the source and sink idiom with unique_ptr, discuss unique_ptr's customizable deleters, and demonstrate how unique_ptr handles arrays.

Blocking The "Bad Guys"

In the first part of this series I explained what might happen when auto_ptr is used in generic code. In short, the problem is that what outwardly appears to be a copy operation is actually a move operation in disguise. When such a disguised move operation takes an lvalue auto_ptr as its righthand side operand, that auto_ptr is effectively emptied, or pilfered. The program doesn't know that that auto_ptr has been pilfered though; it dereferences the pilfered auto_ptr and crashes.

The design of unique_ptr prevents such accidents. Class unique_ptr defines two copy constructors:

template <class T> class unique_ptr
{
public:
 unique_ptr(unique_ptr&& up);   // rvalues bind here
private:
 unique_ptr(const unique_ptr&); // lvalues bind here
};

Although they look like copy-constructors, these are in fact move constructors. They transfer the resource from the source argument to *this. Notice however that while the first move constructor is public, the second one is private. This arrangement allows you to move from rvalues with copy syntax using the move constructor which binds to rvalues (unique_ptr(unique_ptr&& u)), while blocking the copy from lvalues by making the copy constructor that binds to lvalues (unique_ptr(const unique_ptr&)) private.

Now I'll explain it in simpler words. The bad guys are move operations that take a righthand side lvalue argument. They are blocked because the move constructor that intercepts lvalues is private. Moving from an rvalue (which is safe) is intercepted by the public move constructor that takes an rvalue reference.

Why is Moving From An Rvalue Safe?

To make the distinction between rvalues and lvalues simpler, replace these terms with "unnamed objects" and "named objects", respectively. When you move from an unnamed object (e.g., an object returned by value from a function) the unnamed object is destroyed immediately after the move operation, so your code can't access the pilfered object accidentally. In contrast, when you move from a named object, the program might accidentally access that object later, not knowing it has been emptied. It's the latter kind of move operations that unique_ptr disables.

Safe Usage With Containers and Algorithms

The conclusion drawn from the previous paragraph is that you can use unique_ptr safely in generic code. If a container or an algorithm moves from an lvalue element using copy syntax (remember the bad guys?), you will get a compilation error, not a runtime crash. If however the container or algorithm moves elements instead of copying, it will work perfectly:

vector<unique_ptr<char> > vc;
v.push_back(unique_ptr<char>(new int('c'))); 
v.push_back(unique_ptr<char>(new int('b')));
v.push_back(unique_ptr<char>(new int('a')));
sort(v.begin(),v.end(),indirect_less()); //result {a,b,c}


Sinks and Sources

Implementing the source and sink idiom with unique_ptr is straightforward. The source function allocates a resource and wraps it in a unique_ptr returned by value:

unique_ptr<int> source(int i)
{
 return unique_ptr<int>(new int(i));
}

void sink(unique_ptr<int>);
sink(source(2));//Fine, implicit move from rvalue

Notice however that when a "bad guys" type of move takes place, you must use an explicit move() call:

unique_ptr<int> up1=source(2);
sink(up1); //compilation error: implicit move from lvalue
sink(move(up1)); //OK, explicit move

Customizing A Deleter

unique_ptr lets you customize its deleter. Recall that a deleter is a callable entity that a smart pointer's destructor will invoke to deallocate its resource. The default deleter of unique_ptr calls delete. However, when the resource isn't an object allocated by new you can override the default. For example, binding a unique_ptr to an object allocated by malloc() requires a deleter that calls free():

int* pi=(int*)malloc(sizeof(int))
unique_ptr<pu, std::free> myptr;
*myptr=5;

Handling Arrays

As opposed to what most programmers believe, it's not enough to replace delete with delete[] in order to make a smart pointer handle arrays properly. The interface of smart pointers to single objects differs fundamentally from that of smart pointers to arrays in at least three crucial aspects:

  • A single object smart pointer usually supports derived-to-base conversions while array smart pointers must not support such conversions.
  • Defining a dereference operator for a single object makes sense but not for an array as it will dereference only the first element.
  • The [] operator makes sense only in an array smart pointer.

In other words, a unique_ptr class for handling arrays must have a different implementation from that of the single object unique_ptr. The designers of unique_ptr solved this problem by defining a partial specialization for unique_ptr<T[]>:

unique_ptr <int[]> intarr (new int[4]); //array version of unique_ptr
arrup[0]=10;
arrup[1]=20;
arrup[2]=30;
arrup[3]=40;

The trailing [] after the type T indicates that the array specialization of unique_ptr (no dereference, no conversions, has indexing) shall be used. The default deleter for the array specialization of unique_ptr calls delete[].