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.