- Introduction
- Resources and Resource Leaks
- Class Invariants
- Exception Safety
- More Information
Resources and Resource Leaks
Consider a traditional piece of code:
void use_file(const char* fn) { FILE* f = fopen(fn,"r"); // use f fclose(f); }
This code looks plausible. However, if something goes wrong after the call of fopen() and before the call of fclose(), it's possible to exit use_file() without calling fclose(). In particular, an exception might be thrown in the use f code, or in a function called from there. Even an ordinary return could bypass fclose(f), but that's more likely to be noticed by a programmer or by testing.
A typical first attempt to make use_file() fault-tolerant looks like this:
void use_file(const char* fn) { FILE* f = fopen(fn,"r"); try { // use f } catch (...) { fclose(f); throw; } fclose(f); }
The code using the file is enclosed in a try block that catches every exception, closes the file, and re-throws the exception.
The problem with this solution is that it's ad hoc, verbose, tedious, and potentially expensive. Another problem is that the programmer has to remember to apply this solution everywhere a file is opened, and must get it right every time. Such ad hoc solutions are inherently error-prone. Fortunately, there is a more elegant solution.
It's a fundamental rule that when a variable goes out of scope its destructor is called. This is true even if the scope is exited by an exception. Therefore, if we can get a destructor for a local variable to close the file, we have a solution. For example, we can define a class File_ptr that acts like a FILE*:
class File_ptr { FILE* p; public: File_ptr(const char* n, const char* a) { p = fopen(n,a); } // suitable copy operations ~File_ptr() { if (p) fclose(p); } operator FILE*() { return p; } // extract pointer for use };
Given that, our function shrinks to this minimum:
void use_file(const char* fn) { File_ptr f(fn,"r"); // use f }
The destructor will be called independently of whether the function is exited normally or exited because an exception is thrown. That is, the exception-handling mechanism enables us to remove the error-handling code from the main algorithm. The resulting code is simpler and less error-prone than its traditional counterpart.
The file example is a fairly ordinary resource leak problem. A resource is anything that our code acquires from somewhere and needs to give back. A resource that is not properly "given back'' (released) is said to be leaked. Other examples of common resources are memory, sockets, and thread handles. Resource management is the heart of many programs. Typically, we want to make sure than every resource is properly released, whether we use exceptions or not.
You could say that I have merely shifted the complexity away from the use_file() function into the File_ptr class. That's true, but I need only write the File_ptr once for a program, and I often open files more frequently than that. In general, to use this technique we need one small "resource handle class'' for each kind of resource in a system. Some libraries provide such classes for the resources they offer, so the application programmer is saved that task.
The C++ standard library provides auto_ptr for holding individual objects. It also provides containers, notably vector and string, for managing sequences of objects.
The technique of having a constructor acquire a resource and a destructor release it is usually called resource acquisition is initialization.