- by cj
How should an error during resource deallocation be handled, when the
object representing the resource is contained in a shared pointer?
Smart pointers are a useful tool to manage resources safely. Examples
of such resources are memory, disk files, database connections, or
network connections.
// open a connection to the local HTTP port
boost::shared_ptr<Socket> socket = Socket::connect("localhost:80");
In a typical scenario, the class encapsulating the resource should be
noncopyable and polymorphic. A good way to support this is to provide
a factory method returning a shared pointer, and declare all
constructors non-public. The shared pointers can now be copied from
and assigned to freely. The object is automatically destroyed when no
reference to it remains, and the destructor then releases the
resource.
/** A TCP/IP connection. */
class Socket
{
public:
static boost::shared_ptr<Socket> connect(const std::string& address);
virtual ~Socket();
protected:
Socket(const std::string& address);
private:
// not implemented
Socket(const Socket&);
Socket& operator=(const Socket&);
};
But there is a problem with this approach. The destructor must not
throw, so a failure to release the resource will remain undetected.
A common way out of this problem is to add a public method to release
the resource.
class Socket
{
public:
virtual void close(); // may throw
// ...
};
Unfortunately, this approach introduces another problem: Our objects
may now contain resources which have already been released. This
complicates the implementation of the resource class. Even worse, it
makes it possible for clients of the class to use it incorrectly. The
following example may seem far-fetched, but it is a common pitfall in
multi-threaded code.
socket->close();
// ...
size_t nread = socket->read(&buffer[0], buffer.size()); // wrong use!
Either we ensure that the resource is not released before the object
is destroyed, thereby losing any way to deal with a failed resource
deallocation. Or we provide a way to release the resource explicitly
during the object's lifetime, thereby making it possible to use the
resource class incorrectly.
There is a way out of this dilemma. But the solution involves using a
modified shared pointer class. These modifications are likely to be
controversial.
Typical shared pointer implementations, such as boost::shared_ptr,
require that no exception be thrown when their object's destructor is
called. Generally, no destructor should ever throw, so this is a
reasonable requirement. These implementations also allow a custom
deleter function to be specified, which is called in lieu of the
destructor when no reference to the object remains. The no-throw
requirement is extended to this custom deleter function.
The rationale for this requirement is clear: The shared pointer's
destructor must not throw. If the deleter function does not throw, nor
will the shared pointer's destructor. However, the same holds for
other member functions of the shared pointer which lead to resource
deallocation, e.g. reset(): If resource deallocation fails, no
exception can be thrown.
The solution proposed here is to allow custom deleter functions to
throw. This means that the modified shared pointer's destructor must
catch exceptions thrown by the deleter function. On the other hand,
member functions other than the destructor, e.g. reset(), shall not
catch exceptions of the deleter function (and their implementation
becomes somewhat more complicated).
Here is the original example, using a throwing deleter function:
/** A TCP/IP connection. */
class Socket
{
public:
static SharedPtr<Socket> connect(const std::string& address);
protected:
Socket(const std::string& address);
virtual Socket() { }
private:
struct Deleter;
// not implemented
Socket(const Socket&);
Socket& operator=(const Socket&);
};
struct Socket::Deleter
{
void operator()(Socket* socket)
{
// Close the connection. If an error occurs, delete the socket
// and throw an exception.
delete socket;
}
};
SharedPtr<Socket> Socket::connect(const std::string& address)
{
return SharedPtr<Socket>(new Socket(address), Deleter());
}
We can now use reset() to free the resource explicitly. If there is
still a reference to the resource in another thread or another part of
the program, calling reset() will only decrement the reference
count. If this is the last reference to the resource, the resource is
released. If resource deallocation fails, an exception is thrown.
SharedPtr<Socket> socket = Socket::connect("localhost:80");
// ...
socket.reset();