12.1.4. Smart Pointers and Exceptions

Image

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.

Smart Pointers and Dumb Classes
Image

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.

Using Our Own Deletion Code
Image

By default, shared_ptrs 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.


Exercises Section 12.1.4

Exercise 12.14: Write your own version of a function that uses a shared_ptr to manage a connection.

Exercise 12.15: Rewrite the first exercise to use a lambda (§ 10.3.2, p. 388) in place of the end_connection function.


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

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