In C++, dynamic memory is managed through a pair of operators: new
, which allocates, and optionally initializes, an object in dynamic memory and returns a pointer to that object; and delete
, which takes a pointer to a dynamic object, destroys that object, and frees the associated memory.
Dynamic memory is problematic because it is surprisingly hard to ensure that we free memory at the right time. Either we forget to free the memory—in which case we have a memory leak—or we free the memory when there are still pointers referring to that memory—in which case we have a pointer that refers to memory that is no longer valid.
To make using dynamic memory easier (and safer), the new library provides two smart pointer types that manage dynamic objects. A smart pointer acts like a regular pointer with the important exception that it automatically deletes the object to which it points. The new library defines two kinds of smart pointers that differ in how they manage their underlying pointers: shared_ptr
, which allows multiple pointers to refer to the same object, and unique_ptr
, which “owns” the object to which it points. The library also defines a companion class named weak_ptr
that is a weak reference to an object managed by a shared_ptr
. All three are defined in the memory
header.
shared_ptr
ClassLike vector
s, smart pointers are templates (§ 3.3, p. 96). Therefore, when we create a smart pointer, we must supply additional information—in this case, the type to which the pointer can point. As with vector
, we supply that type inside angle brackets that follow the name of the kind of smart pointer we are defining:
shared_ptr<string> p1; // shared_ptr that can point at a string
shared_ptr<list<int>> p2; // shared_ptr that can point at a list of ints
A default initialized smart pointer holds a null pointer (§ 2.3.2, p. 53). In § 12.1.3 (p. 464), we’ll cover additional ways to initialize a smart pointer.
We use a smart pointer in ways that are similar to using a pointer. Dereferencing a smart pointer returns the object to which the pointer points. When we use a smart pointer in a condition, the effect is to test whether the pointer is null:
// if p1 is not null, check whether it's the empty string
if (p1 && p1->empty())
*p1 = "hi"; // if so, dereference p1 to assign a new value to that string
Table 12.1 (overleaf) lists operations common to shared_ptr
and unique_ptr
. Those that are particular to shared_ptr
are listed in Table 12.2 (p. 453).
Table 12.1. Operations Common to shared_ptr
and unique_ptr
Table 12.2. Operations Specific to shared_ptr
make_shared
FunctionThe safest way to allocate and use dynamic memory is to call a library function named make_shared
. This function allocates and initializes an object in dynamic memory and returns a shared_ptr
that points to that object. Like the smart pointers, make_shared
is defined in the memory
header.
When we call make_shared
, we must specify the type of object we want to create. We do so in the same way as we use a template class, by following the function name with a type enclosed in angle brackets:
// shared_ptr that points to an int with value 42
shared_ptr<int> p3 = make_shared<int>(42);
// p4 points to a string with value 9999999999
shared_ptr<string> p4 = make_shared<string>(10, '9');
// p5 points to an int that is value initialized (§ 3.3.1 (p. 98)) to 0
shared_ptr<int> p5 = make_shared<int>();
Like the sequential-container emplace
members (§ 9.3.1, p. 345), make_shared
uses its arguments to construct an object of the given type. For example, a call to make_shared<string>
must pass argument(s) that match one of the string
constructors. Calls to make_shared<int>
can pass any value we can use to initialize an int
. And so on. If we do not pass any arguments, then the object is value initialized (§ 3.3.1, p. 98).
Of course, ordinarily we use auto
(§ 2.5.2, p. 68) to make it easier to define an object to hold the result of make_shared
:
// p6 points to a dynamically allocated, empty vector<string>
auto p6 = make_shared<vector<string>>();
shared_ptr
sWhen we copy or assign a shared_ptr
, each shared_ptr
keeps track of how many other shared_ptr
s point to the same object:
auto p = make_shared<int>(42); // object to which p points has one user
auto q(p); // p and q point to the same object
// object to which p and q point has two users
We can think of a shared_ptr
as if it has an associated counter, usually referred to as a reference count. Whenever we copy a shared_ptr
, the count is incremented. For example, the counter associated with a shared_ptr
is incremented when we use it to initialize another shared_ptr
, when we use it as the right-hand operand of an assignment, or when we pass it to (§ 6.2.1, p. 209) or return it from a function by value (§ 6.3.2, p. 224). The counter is decremented when we assign a new value to the shared_ptr
and when the shared_ptr
itself is destroyed, such as when a local shared_ptr
goes out of scope (§ 6.1.1, p. 204).
Once a shared_ptr
’s counter goes to zero, the shared_ptr
automatically frees the object that it manages:
auto r = make_shared<int>(42); // int to which r points has one user
r = q; // assign to r, making it point to a different address
// increase the use count for the object to which q points
// reduce the use count of the object to which r had pointed
// the object r had pointed to has no users; that object is automatically freed
Here we allocate an int
and store a pointer to that int
in r
. Next, we assign a new value to r
. In this case, r
is the only shared_ptr
pointing to the one we previously allocated. That int
is automatically freed as part of assigning q
to r
.
It is up to the implementation whether to use a counter or another data structure to keep track of how many pointers share state. The key point is that the class keeps track of how many
shared_ptr
s point to the same object and automatically frees that object when appropriate.
shared_ptr
s Automatically Destroy Their Objects ...When the last shared_ptr
pointing to an object is destroyed, the shared_ptr
class automatically destroys the object to which that shared_ptr
points. It does so through another special member function known as a destructor. Analogous to its constructors, each class has a destructor. Just as a constructor controls initialization, the destructor controls what happens when objects of that class type are destroyed.
Destructors generally free the resources that an object has allocated. For example, the string
constructors (and other string
members) allocate memory to hold the characters that compose the string
. The string
destructor frees that memory. Similarly, several vector
operations allocate memory to hold the elements in the vector
. The destructor for vector
destroys those elements and frees the memory used for the elements.
The destructor for shared_ptr
decrements the reference count of the object to which that shared_ptr
points. If the count goes to zero, the shared_ptr
destructor destroys the object to which the shared_ptr
points and frees the memory used by that object.
The fact that the shared_ptr
class automatically frees dynamic objects when they are no longer needed makes it fairly easy to use dynamic memory. For example, we might have a function that returns a shared_ptr
to a dynamically allocated object of a type named Foo
that can be initialized by an argument of type T
:
// factory returns a shared_ptr pointing to a dynamically allocated object
shared_ptr<Foo> factory(T arg)
{
// process arg as appropriate
// shared_ptr will take care of deleting this memory
return make_shared<Foo>(arg);
}
Because factory
returns a shared_ptr
, we can be sure that the object allocated by factory
will be freed when appropriate. For example, the following function stores the shared_ptr
returned by factory
in a local variable:
void use_factory(T arg)
{
shared_ptr<Foo> p = factory(arg);
// use p
} // p goes out of scope; the memory to which p points is automatically freed
Because p
is local to use_factory
, it is destroyed when use_factory
ends (§ 6.1.1, p. 204). When p
is destroyed, its reference count is decremented and checked. In this case, p
is the only object referring to the memory returned by factory
. Because p
is about to go away, the object to which p
points will be destroyed and the memory in which that object resides will be freed.
The memory will not be freed if there is any other shared_ptr
pointing to it:
shared_ptr<Foo> use_factory(T arg)
{
shared_ptr<Foo> p = factory(arg);
// use p
return p; // reference count is incremented when we return p
} // p goes out of scope; the memory to which p points is not freed
In this version, the return
statement in use_factory
returns a copy of p
to its caller (§ 6.3.2, p. 224). Copying a shared_ptr
adds to the reference count of that object. Now when p
is destroyed, there will be another user for the memory to which p
points. The shared_ptr
class ensures that so long as there are any shared_ptr
s attached to that memory, the memory itself will not be freed.
Because memory is not freed until the last shared_ptr
goes away, it can be important to be sure that shared_ptr
s don’t stay around after they are no longer needed. The program will execute correctly but may waste memory if you neglect to destroy shared_ptr
s that the program does not need. One way that shared_ptr
s might stay around after you need them is if you put shared_ptr
s in a container and subsequently reorder the container so that you don’t need all the elements. You should be sure to erase shared_ptr
elements once you no longer need those elements.
If you put
shared_ptr
s in a container, and you subsequently need to use some, but not all, of the elements, remember toerase
the elements you no longer need.
Programs tend to use dynamic memory for one of three purposes:
1. They don’t know how many objects they’ll need
2. They don’t know the precise type of the objects they need
3. They want to share data between several objects
The container classes are an example of classes that use dynamic memory for the first purpose and we’ll see examples of the second in Chapter 15. In this section, we’ll define a class that uses dynamic memory in order to let several objects share the same underlying data.
So far, the classes we’ve used allocate resources that exist only as long as the corresponding objects. For example, each vector
“owns” its own elements. When we copy a vector
, the elements in the original vector
and in the copy are separate from one another:
vector<string> v1; // empty vector
{ // new scope
vector<string> v2 = {"a", "an", "the"};
v1 = v2; // copies the elements from v2 into v1
} // v2 is destroyed, which destroys the elements in v2
// v1 has three elements, which are copies of the ones originally in v2
The elements allocated by a vector
exist only while the vector
itself exists. When a vector
is destroyed, the elements in the vector
are also destroyed.
Some classes allocate resources with a lifetime that is independent of the original object. As an example, assume we want to define a class named Blob
that will hold a collection of elements. Unlike the containers, we want Blob
objects that are copies of one another to share the same elements. That is, when we copy a Blob
, the original and the copy should refer to the same underlying elements.
In general, when two objects share the same underlying data, we can’t unilaterally destroy the data when an object of that type goes away:
Blob<string> b1; // empty Blob
{ // new scope
Blob<string> b2 = {"a", "an", "the"};
b1 = b2; // b1 and b2 share the same elements
} // b2 is destroyed, but the elements in b2 must not be destroyed
// b1 points to the elements originally created in b2
In this example, b1
and b2
share the same elements. When b2
goes out of scope, those elements must stay around, because b1
is still using them.
StrBlob
ClassUltimately, we’ll implement our Blob
class as a template, but we won’t learn how to do so until § 16.1.2 (p. 658). For now, we’ll define a version of our class that can manage string
s. As a result, we’ll name this version of our class StrBlob
.
The easiest way to implement a new collection type is to use one of the library containers to manage the elements. That way, we can let the library type manage the storage for the elements themselves. In this case, we’ll use a vector
to hold our elements.
However, we can’t store the vector
directly in a Blob
object. Members of an object are destroyed when the object itself is destroyed. For example, assume that b1
and b2
are two Blob
s that share the same vector
. If that vector
were stored in one of those Blobs
—say, b2
—then that vector
, and therefore its elements, would no longer exist once b2
goes out of scope. To ensure that the elements continue to exist, we’ll store the vector
in dynamic memory.
To implement the sharing we want, we’ll give each StrBlob
a shared_ptr
to a dynamically allocated vector
. That shared_ptr
member will keep track of how many StrBlobs
share the same vector
and will delete the vector
when the last StrBlob
using that vector
is destroyed.
We still need to decide what operations our class will provide. For now, we’ll implement a small subset of the vector
operations. We’ll also change the operations that access elements (e.g., front
and back
): In our class, these operations will throw an exception if a user attempts to access an element that doesn’t exist.
Our class will have a default constructor and a constructor that has a parameter of type initializer_list<string>
(§ 6.2.6, p. 220). This constructor will take a braced list of initializers.
class StrBlob {
public:
typedef std::vector<std::string>::size_type size_type;
StrBlob();
StrBlob(std::initializer_list<std::string> il);
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
// add and remove elements
void push_back(const std::string &t) {data->push_back(t);}
void pop_back();
// element access
std::string& front();
std::string& back();
private:
std::shared_ptr<std::vector<std::string>> data;
// throws msg if data[i] isn't valid
void check(size_type i, const std::string &msg) const;
};
Inside the class we implemented the size
, empty
, and push_back
members. These members forward their work through the data
pointer to the underlying vector
. For example, size()
on a StrBlob
calls data->size()
, and so on.
StrBlob
ConstructorsEach constructor uses its constructor initializer list (§ 7.1.4, p. 265) to initialize its data
member to point to a dynamically allocated vector
. The default constructor allocates an empty vector
:
StrBlob::StrBlob(): data(make_shared<vector<string>>()) { }
StrBlob::StrBlob(initializer_list<string> il):
data(make_shared<vector<string>>(il)) { }
The constructor that takes an initializer_list
passes its parameter to the corresponding vector
constructor (§ 2.2.1, p. 43). That constructor initializes the vector
’s elements by copying the values in the list.
The pop_back
, front
, and back
operations access members in the vector
. These operations must check that an element exists before attempting to access that element. Because several members need to do the same checking, we’ve given our class a private
utility function named check
that verifies that a given index is in range. In addition to an index, check
takes a string
argument that it will pass to the exception handler. The string
describes what went wrong:
void StrBlob::check(size_type i, const string &msg) const
{
if (i >= data->size())
throw out_of_range(msg);
}
The pop_back
and element access members first call check
. If check
succeeds, these members forward their work to the underlying vector
operation:
string& StrBlob::front()
{
// if the vector is empty, check will throw
check(0, "front on empty StrBlob");
return data->front();
}
string& StrBlob::back()
{
check(0, "back on empty StrBlob");
return data->back();
}
void StrBlob::pop_back()
{
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
The front
and back
members should be overloaded on const
(§ 7.3.2, p. 276). Defining those versions is left as an exercise.
StrBlob
sLike our Sales_data
class, StrBlob
uses the default versions of the operations that copy, assign, and destroy objects of its type (§ 7.1.5, p. 267). By default, these operations copy, assign, and destroy the data members of the class. Our StrBlob
has only one data member, which is a shared_ptr
. Therefore, when we copy, assign, or destroy a StrBlob
, its shared_ptr
member will be copied, assigned, or destroyed.
As we’ve seen, copying a shared_ptr
increments its reference count; assigning one shared_ptr
to another increments the count of the right-hand operand and decrements the count in the left-hand operand; and destroying a shared_ptr
decrements the count. If the count in a shared_ptr
goes to zero, the object to which that shared_ptr
points is automatically destroyed. Thus, the vector
allocated by the StrBlob
constructors will be automatically destroyed when the last StrBlob
pointing to that vector
is destroyed.
Exercises Section 12.1.1
Exercise 12.1: How many elements do
b1
andb2
have at the end of this code?StrBlob b1;
{
StrBlob b2 = {"a", "an", "the"};
b1 = b2;
b2.push_back("about");
}Exercise 12.2: Write your own version of the
StrBlob
class including theconst
versions offront
andback
.Exercise 12.3: Does this class need
const
versions ofpush_back
andpop_back
? If so, add them. If not, why aren’t they needed?Exercise 12.4: In our
check
function we didn’t check whetheri
was greater than zero. Why is it okay to omit that check?Exercise 12.5: We did not make the constructor that takes an
initializer_list explicit
(§ 7.5.4, p. 296). Discuss the pros and cons of this design choice.
The language itself defines two operators that allocate and free dynamic memory. The new
operator allocates memory, and delete
frees memory allocated by new
.
For reasons that will become clear as we describe how these operators work, using these operators to manage memory is considerably more error-prone than using a smart pointer. Moreover, classes that do manage their own memory—unlike those that use smart pointers—cannot rely on the default definitions for the members that copy, assign, and destroy class objects (§ 7.1.4, p. 264). As a result, programs that use smart pointers are likely to be easier to write and debug.
Until you have read Chapter 13, your classes should allocate dynamic memory only if they use smart pointers to manage that memory.
new
to Dynamically Allocate and Initialize ObjectsObjects allocated on the free store are unnamed, so new
offers no way to name the objects that it allocates. Instead, new
returns a pointer to the object it allocates:
int *pi = new int; // pi points to a dynamically allocated,
// unnamed, uninitialized int
This new
expression constructs an object of type int
on the free store and returns a pointer to that object.
By default, dynamically allocated objects are default initialized (§ 2.2.1, p. 43), which means that objects of built-in or compound type have undefined value; objects of class type are initialized by their default constructor:
string *ps = new string; // initialized to empty string
int *pi = new int; // pi points to an uninitialized int
We can initialize a dynamically allocated object using direct initialization (§ 3.2.1, p. 84). We can use traditional construction (using parentheses), and under the new standard, we can also use list initialization (with curly braces):
int *pi = new int(1024); // object to which pi points has value 1024
string *ps = new string(10, '9'); // *ps is "9999999999"
// vector with ten elements with values from 0 to 9
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};
We can also value initialize (§ 3.3.1, p. 98) a dynamically allocated object by following the type name with a pair of empty parentheses:
string *ps1 = new string; // default initialized to the empty string
string *ps = new string(); // value initialized to the empty string
int *pi1 = new int; // default initialized; *pi1 is undefined
int *pi2 = new int(); // value initialized to 0; *pi2 is 0
For class types (such as string
) that define their own constructors (§ 7.1.4, p. 262), requesting value initialization is of no consequence; regardless of form, the object is initialized by the default constructor. In the case of built-in types the difference is significant; a value-initialized object of built-in type has a well-defined value but a default-initialized object does not. Similarly, members of built-in type in classes that rely on the synthesized default constructor will also be uninitialized if those members are not initialized in the class body (§ 7.1.4, p. 263).
For the same reasons as we usually initialize variables, it is also a good idea to initialize dynamically allocated objects.
When we provide an initializer inside parentheses, we can use auto
(§ 2.5.2, p. 68) to deduce the type of the object we want to allocate from that initializer. However, because the compiler uses the initializer’s type to deduce the type to allocate, we can use auto
only with a single initializer inside parentheses:
auto p1 = new auto(obj); // p points to an object of the type of obj
// that object is initialized from obj
auto p2 = new auto{a,b,c}; // error: must use parentheses for the initializer
The type of p1
is a pointer to the auto
-deduced type of obj
. If obj
is an int
, then p1
is int*;
if obj
is a string
, then p1
is a string*;
and so on. The newly allocated object is initialized from the value of obj
.
const
ObjectsIt is legal to use new
to allocate const
objects:
// allocate and initialize a const int
const int *pci = new const int(1024);
// allocate a default-initialized const empty string
const string *pcs = new const string;
Like any other const
, a dynamically allocated const
object must be initialized. A const
dynamic object of a class type that defines a default constructor (§ 7.1.4, p. 263) may be initialized implicitly. Objects of other types must be explicitly initialized. Because the allocated object is const
, the pointer returned by new
is a pointer to const
(§ 2.4.2, p. 62).
Although modern machines tend to have huge memory capacity, it is always possible that the free store will be exhausted. Once a program has used all of its available memory, new
expressions will fail. By default, if new
is unable to allocate the requested storage, it throws an exception of type bad_alloc
(§ 5.6, p. 193). We can prevent new
from throwing an exception by using a different form of new
:
// if allocation fails, new returns a null pointer
int *p1 = new int; // if allocation fails, new throws std::bad_alloc
int *p2 = new (nothrow) int; // if allocation fails, new returns a null pointer
For reasons we’ll explain in § 19.1.2 (p. 824) this form of new
is referred to as placement new. A placement new expression lets us pass additional arguments to new
. In this case, we pass an object named nothrow
that is defined by the library. When we pass nothrow
to new
, we tell new
that it must not throw an exception. If this form of new
is unable to allocate the requested storage, it will return a null pointer. Both bad_alloc
and nothrow
are defined in the new
header.
In order to prevent memory exhaustion, we must return dynamically allocated memory to the system once we are finished using it. We return memory through a delete
expression. A delete
expression takes a pointer to the object we want to free:
delete p; // p must point to a dynamically allocated object or be null
Like new
, a delete
expression performs two actions: It destroys the object to which its given pointer points, and it frees the corresponding memory.
delete
The pointer we pass to delete
must either point to dynamically allocated memory or be a null pointer (§ 2.3.2, p. 53). Deleting a pointer to memory that was not allocated by new
, or deleting the same pointer value more than once, is undefined:
int i, *pi1 = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i; // error: i is not a pointer
delete pi1; // undefined: pi1 refers to a local
delete pd; // ok
delete pd2; // undefined: the memory pointed to by pd2 was already freed
delete pi2; // ok: it is always ok to delete a null pointer
The compiler will generate an error for the delete
of i
because it knows that i
is not a pointer. The errors associated with executing delete
on pi1
and pd2
are more insidious: In general, compilers cannot tell whether a pointer points to a statically or dynamically allocated object. Similarly, the compiler cannot tell whether memory addressed by a pointer has already been freed. Most compilers will accept these delete
expressions, even though they are in error.
Although the value of a const
object cannot be modified, the object itself can be destroyed. As with any other dynamic object, a const
dynamic object is freed by executing delete
on a pointer that points to that object:
const int *pci = new const int(1024);
delete pci; // ok: deletes a const object
As we saw in § 12.1.1 (p. 452), memory that is managed through a shared_ptr
is automatically deleted when the last shared_ptr
is destroyed. The same is not true for memory we manage using built-in pointers. A dynamic object managed through a built-in pointer exists until it is explicitly deleted.
Functions that return pointers (rather than smart pointers) to dynamic memory put a burden on their callers—the caller must remember to delete the memory:
// factory returns a pointer to a dynamically allocated object
Foo* factory(T arg)
{
// process arg as appropriate
return new Foo(arg); // caller is responsible for deleting this memory
}
Like our earlier factory
function (§ 12.1.1, p. 453), this version of factory
allocates an object but does not delete
it. Callers of factory
are responsible for freeing this memory when they no longer need the allocated object. Unfortunately, all too often the caller forgets to do so:
void use_factory(T arg)
{
Foo *p = factory(arg);
// use p but do not delete it
} // p goes out of scope, but the memory to which p points is not freed!
Here, our use_factory
function calls factory
, which allocates a new object of type Foo
. When use_factory
returns, the local variable p
is destroyed. That variable is a built-in pointer, not a smart pointer.
Unlike class types, nothing happens when objects of built-in type are destroyed. In particular, when a pointer goes out of scope, nothing happens to the object to which the pointer points. If that pointer points to dynamic memory, that memory is not automatically freed.
Dynamic memory managed through built-in pointers (rather than smart pointers) exists until it is explicitly freed.
In this example, p
was the only pointer to the memory allocated by factory
. Once use_factory
returns, the program has no way to free that memory. Depending on the logic of our overall program, we should fix this bug by remembering to free the memory inside use_factory
:
void use_factory(T arg)
{
Foo *p = factory(arg);
// use p
delete p; // remember to free the memory now that we no longer need it
}
or, if other code in our system needs to use the object allocated by use_factory
, we should change that function to return a pointer to the memory it allocated:
Foo* use_factory(T arg)
{
Foo *p = factory(arg);
// use p
return p; // caller must delete the memory
}
There are three common problems with using
new
anddelete
to manage dynamic memory:1. Forgetting to
delete
memory. Neglecting to delete dynamic memory is known as a “memory leak,” because the memory is never returned to the free store. Testing for memory leaks is difficult because they usually cannot be detected until the application is run for a long enough time to actually exhaust memory.2. Using an object after it has been deleted. This error can sometimes be detected by making the pointer null after the delete.
3. Deleting the same memory twice. This error can happen when two pointers address the same dynamically allocated object. If
delete
is applied to one of the pointers, then the object’s memory is returned to the free store. If we subsequentlydelete
the second pointer, then the free store may be corrupted.These kinds of errors are considerably easier to make than they are to find and fix.
You can avoid all of these problems by using smart pointers exclusively. The smart pointer will take care of deleting the memory only when there are no remaining smart pointers pointing to that memory.
delete ...
When we delete
a pointer, that pointer becomes invalid. Although the pointer is invalid, on many machines the pointer continues to hold the address of the (freed) dynamic memory. After the delete
, the pointer becomes what is referred to as a dangling pointer. A dangling pointer is one that refers to memory that once held an object but no longer does so.
Dangling pointers have all the problems of uninitialized pointers (§ 2.3.2, p. 54). We can avoid the problems with dangling pointers by deleting the memory associated with a pointer just before the pointer itself goes out of scope. That way there is no chance to use the pointer after the memory associated with the pointer is freed. If we need to keep the pointer around, we can assign nullptr
to the pointer after we use delete
. Doing so makes it clear that the pointer points to no object.
A fundamental problem with dynamic memory is that there can be several pointers that point to the same memory. Resetting the pointer we use to delete
that memory lets us check that particular pointer but has no effect on any of the other pointers that still point at the (freed) memory. For example:
int *p(new int(42)); // p points to dynamic memory
auto q = p; // p and q point to the same memory
delete p; // invalidates both p and q
p = nullptr; // indicates that p is no longer bound to an object
Here both p
and q
point at the same dynamically allocated object. We delete
that memory and set p
to nullptr
, indicating that the pointer no longer points to an object. However, resetting p
has no effect on q
, which became invalid when we deleted the memory to which p
(and q
!) pointed. In real systems, finding all the pointers that point to the same memory is surprisingly difficult.
Exercises Section 12.1.2
Exercise 12.6: Write a function that returns a dynamically allocated
vector
ofint
s. Pass thatvector
to another function that reads the standard input to give values to the elements. Pass thevector
to another function to print the values that were read. Remember todelete
thevector
at the appropriate time.Exercise 12.7: Redo the previous exercise, this time using
shared_ptr
.Exercise 12.8: Explain what if anything is wrong with the following function.
bool b() {
int* p = new int;
// ...
return p;
}int *q = new int(42), *r = new int(100);
r = q;
auto q2 = make_shared<int>(42), r2 = make_shared<int>(100);
r2 = q2;
shared_ptr
s with new
As we’ve seen, if we do not initialize a smart pointer, it is initialized as a null pointer. As described in Table 12.3, we can also initialize a smart pointer from a pointer returned by new
:
shared_ptr<double> p1; // shared_ptr that can point at a double
shared_ptr<int> p2(new int(42)); // p2 points to an int with value 42
Table 12.3. Other Ways to Define and Change shared_ptr
s
The smart pointer constructors that take pointers are explicit
(§ 7.5.4, p. 296). Hence, we cannot implicitly convert a built-in pointer to a smart pointer; we must use the direct form of initialization (§ 3.2.1, p. 84) to initialize a smart pointer:
shared_ptr<int> p1 = new int(1024); // error: must use direct initialization
shared_ptr<int> p2(new int(1024)); // ok: uses direct initialization
The initialization of p1
implicitly asks the compiler to create a shared_ptr
from the int*
returned by new
. Because we can’t implicitly convert a pointer to a smart pointer, this initialization is an error. For the same reason, a function that returns a shared_ptr
cannot implicitly convert a plain pointer in its return statement:
shared_ptr<int> clone(int p) {
return new int(p); // error: implicit conversion to shared_ptr<int>
}
We must explicitly bind a shared_ptr
to the pointer we want to return:
shared_ptr<int> clone(int p) {
// ok: explicitly create a shared_ptr<int> from int*
return shared_ptr<int>(new int(p));
}
By default, a pointer used to initialize a smart pointer must point to dynamic memory because, by default, smart pointers use delete
to free the associated object. We can bind smart pointers to pointers to other kinds of resources. However, to do so, we must supply our own operation to use in place of delete
. We’ll see how to supply our own deletion code in § 12.1.4 (p. 468).
A shared_ptr
can coordinate destruction only with other shared_ptr
s that are copies of itself. Indeed, this fact is one of the reasons we recommend using make_shared
rather than new
. That way, we bind a shared_ptr
to the object at the same time that we allocate it. There is no way to inadvertently bind the same memory to more than one independently created shared_ptr
.
Consider the following function that operates on a shared_ptr
:
// ptr is created and initialized when process is called
void process(shared_ptr<int> ptr)
{
// use ptr
} // ptr goes out of scope and is destroyed
The parameter to process
is passed by value, so the argument to process
is copied into ptr
. Copying a shared_ptr
increments its reference count. Thus, inside process
the count is at least 2. When process
completes, the reference count of ptr
is decremented but cannot go to zero. Therefore, when the local variable ptr
is destroyed, the memory to which ptr
points will not be deleted.
The right way to use this function is to pass it a shared_ptr
:
shared_ptr<int> p(new int(42)); // reference count is 1
process(p); // copying p increments its count; in process the reference count is 2
int i = *p; // ok: reference count is 1
Although we cannot pass a built-in pointer to process
, we can pass process
a (temporary) shared_ptr
that we explicitly construct from a built-in pointer. However, doing so is likely to be an error:
int *x(new int(1024)); // dangerous: x is a plain pointer, not a smart pointer
process(x); // error: cannot convert int* to shared_ptr<int>
process(shared_ptr<int>(x)); // legal, but the memory will be deleted!
int j = *x; // undefined: x is a dangling pointer!
In this call, we passed a temporary shared_ptr
to process
. That temporary is destroyed when the expression in which the call appears finishes. Destroying the temporary decrements the reference count, which goes to zero. The memory to which the temporary points is freed when the temporary is destroyed.
But x
continues to point to that (freed) memory; x
is now a dangling pointer. Attempting to use the value of x
is undefined.
When we bind a shared_ptr
to a plain pointer, we give responsibility for that memory to that shared_ptr
. Once we give shared_ptr
responsibility for a pointer, we should no longer use a built-in pointer to access the memory to which the shared_ptr
now points.
It is dangerous to use a built-in pointer to access an object owned by a smart pointer, because we may not know when that object is destroyed.
get
to Initialize or Assign Another Smart PointerThe smart pointer types define a function named get
(described in Table 12.1 (p. 452)) that returns a built-in pointer to the object that the smart pointer is managing. This function is intended for cases when we need to pass a built-in pointer to code that can’t use a smart pointer. The code that uses the return from get
must not delete
that pointer.
Although the compiler will not complain, it is an error to bind another smart pointer to the pointer returned by get
:
shared_ptr<int> p(new int(42)); // reference count is 1
int *q = p.get(); // ok: but don't use q in any way that might delete its pointer
{ // new block
// undefined: two independent shared_ptrs point to the same memory
shared_ptr<int>(q);
} // block ends, q is destroyed, and the memory to which q points is freed
int foo = *p; // undefined; the memory to which p points was freed
In this case, both p
and q
point to the same memory. Because they were created independently from each other, each has a reference count of 1. When the block in which q
was defined ends, q
is destroyed. Destroying q
frees the memory to which q
points. That makes p
into a dangling pointer, meaning that what happens when we attempt to use p
is undefined. Moreover, when p
is destroyed, the pointer to that memory will be delete
d a second time.
Use
get
only to pass access to the pointer to code that you know will notdelete
the pointer. In particular, never useget
to initialize or assign to another smart pointer.
shared_ptr
OperationsThe shared_ptr
class gives us a few other operations, which are listed in Table 12.2 (p. 453) and Table 12.3 (on the previous page). We can use reset
to assign a new pointer to a shared_ptr
:
p = new int(1024); // error: cannot assign a pointer to a shared_ptr
p.reset(new int(1024)); // ok: p points to a new object
Like assignment, reset
updates the reference counts and, if appropriate, deletes the object to which p
points. The reset
member is often used together with unique
to control changes to the object shared among several shared_ptr
s. Before changing the underlying object, we check whether we’re the only user. If not, we make a new copy before making the change:
if (!p.unique())
p.reset(new string(*p)); // we aren't alone; allocate a new copy
*p += newVal; // now that we know we're the only pointer, okay to change this object
Exercises Section 12.1.3
Exercise 12.10: Explain whether the following call to the
process
function defined on page 464 is correct. If not, how would you correct the call?shared_ptr<int> p(new int(42));
process(shared_ptr<int>(p));Exercise 12.11: What would happen if we called
process
as follows?process(shared_ptr<int>(p.get()));
Exercise 12.12: Using the declarations of
p
andsp
explain each of the following calls toprocess
. If the call is legal, explain what it does. If the call is illegal, explain why:auto p = new int();
auto sp = make_shared<int>();(a)
process(sp);
(b)
process(new int());
(c)
process(p);
(d)
process(shared_ptr<int>(p));
Exercise 12.13: What happens if we execute the following code?
auto sp = make_shared<int>();
auto p = sp.get();
delete p;
In § 5.6.2 (p. 196) we noted that programs that use exception handling to continue processing after an exception occurs need to ensure that resources are properly freed if an exception occurs. One easy way to make sure resources are freed is to use smart pointers.
When we use a smart pointer, the smart pointer class ensures that memory is freed when it is no longer needed even if the block is exited prematurely:
void f()
{
shared_ptr<int> sp(new int(42)); // allocate a new object
// code that throws an exception that is not caught inside f
} // shared_ptr freed automatically when the function ends
When a function is exited, whether through normal processing or due to an exception, all the local objects are destroyed. In this case, sp
is a shared_ptr
, so destroying sp
checks its reference count. Here, sp
is the only pointer to the memory it manages; that memory will be freed as part of destroying sp
.
In contrast, memory that we manage directly is not automatically freed when an exception occurs. If we use built-in pointers to manage memory and an exception occurs after a new
but before the corresponding delete
, then that memory won’t be freed:
void f()
{
int *ip = new int(42); // dynamically allocate a new object
// code that throws an exception that is not caught inside f
delete ip; // free the memory before exiting
}
If an exception happens between the new
and the delete
, and is not caught inside f
, then this memory can never be freed. There is no pointer to this memory outside the function f
. Thus, there is no way to free this memory.
Many C++ classes, including all the library classes, define destructors (§ 12.1.1, p. 452) that take care of cleaning up the resources used by that object. However, not all classes are so well behaved. In particular, classes that are designed to be used by both C and C++ generally require the user to specifically free any resources that are used.
Classes that allocate resources—and that do not define destructors to free those resources—can be subject to the same kind of errors that arise when we use dynamic memory. It is easy to forget to release the resource. Similarly, if an exception happens between when the resource is allocated and when it is freed, the program will leak that resource.
We can often use the same kinds of techniques we use to manage dynamic memory to manage classes that do not have well-behaved destructors. For example, imagine we’re using a network library that is used by both C and C++. Programs that use this library might contain code such as
struct destination; // represents what we are connecting to
struct connection; // information needed to use the connection
connection connect(destination*); // open the connection
void disconnect(connection); // close the given connection
void f(destination &d /* other parameters */)
{
// get a connection; must remember to close it when done
connection c = connect(&d);
// use the connection
// if we forget to call disconnect before exiting f, there will be no way to close c
}
If connection
had a destructor, that destructor would automatically close the connection when f
completes. However, connection
does not have a destructor. This problem is nearly identical to our previous program that used a shared_ptr
to avoid memory leaks. It turns out that we can also use a shared_ptr
to ensure that the connection
is properly closed.
By default, shared_ptr
s assume that they point to dynamic memory. Hence, by default, when a shared_ptr
is destroyed, it executes delete
on the pointer it holds. To use a shared_ptr
to manage a connection
, we must first define a function to use in place of delete
. It must be possible to call this deleter function with the pointer stored inside the shared_ptr
. In this case, our deleter must take a single argument of type connection*
:
void end_connection(connection *p) { disconnect(*p); }
When we create a shared_ptr
, we can pass an optional argument that points to a deleter function (§ 6.7, p. 247):
void f(destination &d /* other parameters */)
{
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
// use the connection
// when f exits, even if by an exception, the connection will be properly closed
}
When p
is destroyed, it won’t execute delete
on its stored pointer. Instead, p
will call end_connection
on that pointer. In turn, end_connection
will call disconnect
, thus ensuring that the connection is closed. If f
exits normally, then p
will be destroyed as part of the return. Moreover, p
will also be destroyed, and the connection will be closed, if an exception occurs.
Smart pointers can provide safety and convenience for handling dynamically allocated memory only when they are used properly. To use smart pointers correctly, we must adhere to a set of conventions:
• Don’t use the same built-in pointer value to initialize (or
reset
) more than one smart pointer.• Don’t
delete
the pointer returned fromget()
.• Don’t use
get()
to initialize orreset
another smart pointer.• If you use a pointer returned by
get()
, remember that the pointer will become invalid when the last corresponding smart pointer goes away.• If you use a smart pointer to manage a resource other than memory allocated by
new
, remember to pass a deleter (§ 12.1.4, p. 468, and § 12.1.5, p. 471).
Exercises Section 12.1.4
Exercise 12.14: Write your own version of a function that uses a
shared_ptr
to manage aconnection
.Exercise 12.15: Rewrite the first exercise to use a lambda (§ 10.3.2, p. 388) in place of the
end_connection
function.
unique_ptr
A unique_ptr
“owns” the object to which it points. Unlike shared_ptr
, only one unique_ptr
at a time can point to a given object. The object to which a unique_ptr
points is destroyed when the unique_ptr
is destroyed. Table 12.4 lists the operations specific to unique_ptr
s. The operations common to both were covered in Table 12.1 (p. 452).
Table 12.4. unique_ptr
Operations (See Also Table 12.1 (p. 452))
Unlike shared_ptr
, there is no library function comparable to make_shared
that returns a unique_ptr
. Instead, when we define a unique_ptr
, we bind it to a pointer returned by new
. As with shared_ptr
s, we must use the direct form of initialization:
unique_ptr<double> p1; // unique_ptr that can point at a double
unique_ptr<int> p2(new int(42)); // p2 points to int with value 42
Because a unique_ptr
owns the object to which it points, unique_ptr
does not support ordinary copy or assignment:
unique_ptr<string> p1(new string("Stegosaurus"));
unique_ptr<string> p2(p1); // error: no copy for unique_ptr
unique_ptr<string> p3;
p3 = p2; // error: no assign for unique_ptr
Although we can’t copy or assign a unique_ptr
, we can transfer ownership from one (nonconst
) unique_ptr
to another by calling release
or reset
:
// transfers ownership from p1 (which points to the string Stegosaurus) to p2
unique_ptr<string> p2(p1.release()); // release makes p1 null
unique_ptr<string> p3(new string("Trex"));
// transfers ownership from p3 to p2
p2.reset(p3.release()); // reset deletes the memory to which p2 had pointed
The release
member returns the pointer currently stored in the unique_ptr
and makes that unique_ptr
null. Thus, p2
is initialized from the pointer value that had been stored in p1
and p1
becomes null.
The reset
member takes an optional pointer and repositions the unique_ptr
to point to the given pointer. If the unique_ptr
is not null, then the object to which the unique_ptr
had pointed is deleted. The call to reset
on p2
, therefore, frees the memory used by the string
initialized from "Stegosaurus"
, transfers p3
’s pointer to p2
, and makes p3
null.
Calling release
breaks the connection between a unique_ptr
and the object it had been managing. Often the pointer returned by release
is used to initialize or assign another smart pointer. In that case, responsibility for managing the memory is simply transferred from one smart pointer to another. However, if we do not use another smart pointer to hold the pointer returned from release
, our program takes over responsibility for freeing that resource:
p2.release(); // WRONG: p2 won't free the memory and we've lost the pointer
auto p = p2.release(); // ok, but we must remember to delete(p)
unique_ptr
sThere is one exception to the rule that we cannot copy a unique_ptr:
We can copy or assign a unique_ptr
that is about to be destroyed. The most common example is when we return a unique_ptr
from a function:
unique_ptr<int> clone(int p) {
// ok: explicitly create a unique_ptr<int> from int*
return unique_ptr<int>(new int(p));
}
Alternatively, we can also return a copy of a local object:
unique_ptr<int> clone(int p) {
unique_ptr<int> ret(new int (p));
// . . .
return ret;
}
In both cases, the compiler knows that the object being returned is about to be destroyed. In such cases, the compiler does a special kind of “copy” which we’ll discuss in § 13.6.2 (p. 534).
Earlier versions of the library included a class named
auto_ptr
that had some, but not all, of the properties ofunique_ptr
. In particular, it was not possible to store anauto_ptr
in a container, nor could we return one from a function.Although
auto_ptr
is still part of the standard library, programs should useunique_ptr
instead.
unique_ptr
Like shared_ptr
, by default, unique_ptr
uses delete
to free the object to which a unique_ptr
points. As with shared_ptr
, we can override the default deleter in a unique_ptr
(§ 12.1.4, p. 468). However, for reasons we’ll describe in § 16.1.6 (p. 676), the way unique_ptr
manages its deleter is differs from the way shared_ptr
does.
Overridding the deleter in a unique_ptr
affects the unique_ptr
type as well as how we construct (or reset
) objects of that type. Similar to overriding the comparison operation of an associative container (§ 11.2.2, p. 425), we must supply the deleter type inside the angle brackets along with the type to which the unique_ptr
can point. We supply a callable object of the specified type when we create or reset
an object of this type:
// p points to an object of type objT and uses an object of type delT to free that object
// it will call an object named fcn of type delT
unique_ptr<objT, delT> p (new objT, fcn);
As a somewhat more concrete example, we’ll rewrite our connection program to use a unique_ptr
in place of a shared_ptr
as follows:
void f(destination &d /* other needed parameters */)
{
connection c = connect(&d); // open the connection
// when p is destroyed, the connection will be closed
unique_ptr<connection, decltype(end_connection)*>
p(&c, end_connection);
// use the connection
// when f exits, even if by an exception, the connection will be properly closed
}
Here we use decltype
(§ 2.5.3, p. 70) to specify the function pointer type. Because decltype(end_connection)
returns a function type, we must remember to add a *
to indicate that we’re using a pointer to that type (§ 6.7, p. 250).
Exercises Section 12.1.5
Exercise 12.16: Compilers don’t always give easy-to-understand error messages if we attempt to copy or assign a
unique_ptr
. Write a program that contains these errors to see how your compiler diagnoses them.Exercise 12.17: Which of the following
unique_ptr
declarations are illegal or likely to result in subsequent program error? Explain what the problem is with each one.int ix = 1024, *pi = &ix, *pi2 = new int(2048);
typedef unique_ptr<int> IntP;(a)
IntP p0(ix);
(b)
IntP p1(pi);
(c)
IntP p2(pi2);
(d)
IntP p3(&ix);
(e)
IntP p4(new int(2048));
(f)
IntP p5(p2.get());
Exercise 12.18: Why doesn’t
shared_ptr
have arelease
member?
weak_ptr
A weak_ptr
(Table 12.5) is a smart pointer that does not control the lifetime of the object to which it points. Instead, a weak_ptr
points to an object that is managed by a shared_ptr
. Binding a weak_ptr
to a shared_ptr
does not change the reference count of that shared_ptr
. Once the last shared_ptr
pointing to the object goes away, the object itself will be deleted. That object will be deleted even if there are weak_ptr
s pointing to it—hence the name weak_ptr
, which captures the idea that a weak_ptr
shares its object “weakly.”
When we create a weak_ptr
, we initialize it from a shared_ptr
:
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp weakly shares with p; use count in p is unchanged
Here both wp
and p
point to the same object. Because the sharing is weak, creating wp
doesn’t change the reference count of p; it is possible that the object to which wp
points might be deleted.
Because the object might no longer exist, we cannot use a weak_ptr
to access its object directly. To access that object, we must call lock
. The lock
function checks whether the object to which the weak_ptr
points still exists. If so, lock
returns a shared_ptr
to the shared object. As with any other shared_ptr
, we are guaranteed that the underlying object to which that shared_ptr
points continues to exist at least as long as that shared_ptr
exists. For example:
if (shared_ptr<int> np = wp.lock()) { // true if np is not null
// inside the if, np shares its object with p
}
Here we enter the body of the if
only if the call to lock
succeeds. Inside the if
, it is safe to use np
to access that object.
As an illustration of when a weak_ptr
is useful, we’ll define a companion pointer class for our StrBlob
class. Our pointer class, which we’ll name StrBlobPtr
, will store a weak_ptr
to the data
member of the StrBlob
from which it was initialized. By using a weak_ptr
, we don’t affect the lifetime of the vector
to which a given StrBlob
points. However, we can prevent the user from attempting to access a vector
that no longer exists.
StrBlobPtr
will have two data members: wptr
, which is either null or points to a vector
in a StrBlob
; and curr
, which is the index of the element that this object currently denotes. Like its companion StrBlob
class, our pointer class has a check
member to verify that it is safe to dereference the StrBlobPtr
:
// StrBlobPtr throws an exception on attempts to access a nonexistent element
class StrBlobPtr {
public:
StrBlobPtr(): curr(0) { }
StrBlobPtr(StrBlob &a, size_t sz = 0):
wptr(a.data), curr(sz) { }
std::string& deref() const;
StrBlobPtr& incr(); // prefix version
private:
// check returns a shared_ptr to the vector if the check succeeds
std::shared_ptr<std::vector<std::string>>
check(std::size_t, const std::string&) const;
// store a weak_ptr, which means the underlying vector might be destroyed
std::weak_ptr<std::vector<std::string>> wptr;
std::size_t curr; // current position within the array
};
The default constructor generates a null StrBlobPtr
. Its constructor initializer list (§ 7.1.4, p. 265) explicitly initializes curr
to zero and implicitly initializes wptr
as a null weak_ptr
. The second constructor takes a reference to StrBlob
and an optional index value. This constructor initializes wptr
to point to the vector
in the shared_ptr
of the given StrBlob
object and initializes curr
to the value of sz
. We use a default argument (§ 6.5.1, p. 236) to initialize curr
to denote the first element by default. As we’ll see, the sz
parameter will be used by the end
member of StrBlob
.
It is worth noting that we cannot bind a StrBlobPtr
to a const StrBlob
object. This restriction follows from the fact that the constructor takes a reference to a nonconst
object of type StrBlob
.
The check
member of StrBlobPtr
differs from the one in StrBlob
because it must check whether the vector
to which it points is still around:
std::shared_ptr<std::vector<std::string>>
StrBlobPtr::check(std::size_t i, const std::string &msg) const
{
auto ret = wptr.lock(); // is the vector still around?
if (!ret)
throw std::runtime_error("unbound StrBlobPtr");
if (i >= ret->size())
throw std::out_of_range(msg);
return ret; // otherwise, return a shared_ptr to the vector
}
Because a weak_ptr
does not participate in the reference count of its corresponding shared_ptr
, the vector
to which this StrBlobPtr
points might have been deleted. If the vector
is gone, lock
will return a null pointer. In this case, any reference to the vector
will fail, so we throw an exception. Otherwise, check
verifies its given index. If that value is okay, check
returns the shared_ptr
it obtained from lock
.
We’ll learn how to define our own operators in Chapter 14. For now, we’ve defined functions named deref
and incr
to dereference and increment the StrBlobPtr
, respectively.
The deref
member calls check
to verify that it is safe to use the vector
and that curr
is in range:
std::string& StrBlobPtr::deref() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr]; // (*p) is the vector to which this object points
}
If check
succeeds, p
is a shared_ptr
to the vector
to which this StrBlobPtr
points. The expression (*p)[curr]
dereferences that shared_ptr
to get the vector
and uses the subscript operator to fetch and return the element at curr
.
The incr
member also calls check
:
// prefix: return a reference to the incremented object
StrBlobPtr& StrBlobPtr::incr()
{
// if curr already points past the end of the container, can't increment it
check(curr, "increment past end of StrBlobPtr");
++curr; // advance the current state
return *this;
}
Of course, in order to access the data
member, our pointer class will have to be a friend
of StrBlob
(§ 7.3.4, p. 279). We’ll also give our StrBlob
class begin
and end
operations that return a StrBlobPtr
pointing to itself:
// forward declaration needed for friend declaration in StrBlob
class StrBlobPtr;
class StrBlob {
friend class StrBlobPtr;
// other members as in § 12.1.1 (p. 456)
// return StrBlobPtr to the first and one past the last elements
StrBlobPtr begin() { return StrBlobPtr(*this); }
StrBlobPtr end()
{ auto ret = StrBlobPtr(*this, data->size());
return ret; }
};
Exercises Section 12.1.6
Exercise 12.19: Define your own version of
StrBlobPtr
and update yourStrBlob
class with the appropriatefriend
declaration andbegin
andend
members.Exercise 12.20: Write a program that reads an input file a line at a time into a
StrBlob
and uses aStrBlobPtr
to print each element in thatStrBlob
.Exercise 12.21: We could have written
StrBlobPtr
’sderef
member as follows:std::string& deref() const
{ return (*check(curr, "dereference past end"))[curr]; }Which version do you think is better and why?
Exercise 12.22: What changes would need to be made to
StrBlobPtr
to create a class that can be used with aconst StrBlob
? Define a class namedConstStrBlobPtr
that can point to aconst StrBlob
.