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).
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 to erase
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.
Exercise 12.1: How many elements do b1
and b2
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 the const
versions of front
and back
.
Exercise 12.3: Does this class need const
versions of push_back
and pop_back
? If so, add them. If not, why aren’t they needed?
Exercise 12.4: In our check
function we didn’t check whether i
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.