Professional C__ - Marc Gregoire [169]
With smart pointers, you never have to remember to free the underlying resource: the smart pointer destructor does it for you, whether you leave the function via an exception or leave the function normally.
Catch, Cleanup, and Rethrow
The next technique for avoiding memory and resource leaks is for each function to catch any possible exceptions, perform necessary cleanup work, and rethrow the exception for the function higher up the stack to handle. Here is a revised funcOne() with this technique:
void funcOne() throw(exception)
{
string str1;
string* str2 = new string();
try {
funcTwo();
} catch (...) {
delete str2;
throw; // Rethrow the exception.
}
delete str2;
}
Code snippet from StackUnwinding\CatchAndRethrow.cpp
This function wraps the call to funcTwo() with an exception handler that performs the cleanup (calls delete on str2) and then rethrows the exception. The keyword throw by itself rethrows whatever exception was caught most recently. Note that the catch statement uses the ... syntax to catch any exception.
This method works fine, but can be messy. In particular, note that there are now two identical lines that call delete on str2: one to handle the exception and one if the function exits normally.
The preferred solution is to use smart pointers instead of the catch, cleanup, and rethrow technique.
COMMON ERROR-HANDLING ISSUES
Whether or not you use exceptions in your programs is up to you and your colleagues. However, we strongly encourage you to formalize an error-handling plan for your programs, regardless of your use of exceptions. If you use exceptions, it is generally easier to come up with a unified error-handling scheme, but it is not impossible without exceptions. The most important aspect of a good plan is uniformity of error handling throughout all the modules of the program. Make sure that every programmer on the project understands and follows the error-handling rules.
This section discusses the most common error-handling issues in the context of exceptions, but the issues are also relevant to programs that do not use exceptions.
Memory Allocation Errors
Despite the fact that all of our examples so far in this book have ignored the possibility, memory allocation can, and will, fail. However, production code must account for memory allocation failures. C++ provides several different ways to handle memory errors.
The default behaviors of new and new[] are to throw an exception of type bad_alloc, defined in the Thus, all your new statements should look something like this: try { ptr = new int[numInts]; } catch (const bad_alloc& e) { cerr << __FILE__ << "(" << __LINE__ << "): Unable to allocate memory!" << endl; // Handle memory allocation failure. return; } // Proceed with function that assumes memory has been allocated. Code snippet from NewFailures\Exceptions.cpp Note that this code uses the predefined preprocessor symbols __FILE__ and __LINE__ which will be replaced with the name of the file and the current line number. This makes debugging easier. This example prints an error message to cerr. This assumes your program is running with a console. In GUI applications, you often don’t have a console in which case you need to show the error in a GUI specific way to the user. You could, of course, bulk handle many possible new failures with a single try/catch block at a higher point in the program, if it will work for your program. Another consideration is that logging an error might try to allocate memory. If new fails, there might not be enough memory left even to log the error message. Non-Throwing new As Chapter 21 explains, if you don’t like exceptions, you can revert to the old C model in which memory allocation routines return a null pointer if they cannot allocate memory. C++ provides nothrow versions of new and new[], which return nullptr instead