12.1.3. Using shared_ptrs 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_ptrs

Image

The smart pointer constructors that take pointers are explicit7.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).

Don’t Mix Ordinary Pointers and Smart Pointers ...
Image

A shared_ptr can coordinate destruction only with other shared_ptrs 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.


Image Warning

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.


...and Don’t Use get to Initialize or Assign Another Smart Pointer
Image

The 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 deleted a second time.


Image Warning

Use get only to pass access to the pointer to code that you know will not delete the pointer. In particular, never use get to initialize or assign to another smart pointer.


Other shared_ptr Operations

The 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_ptrs. 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 and sp explain each of the following calls to process. 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;


..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.189.186