As with almost everything that has benefits, there are some potential downsides to exceptions as well. This article is not meant to be comprehensive, but just to point out some of the major issues that should be considered when using exceptions (or deciding whether to use them).
Cleaning up resources
One of the biggest problems that new programmers run into when using exceptions is the issue of cleaning up resources when an exception occurs. Consider the following example:
1
2
3
4
5
6
7
8
9
10
|
try
{
OpenFile(strFilename);
WriteFile(strFilename, strData);
CloseFile(strFilename);
}
catch
(FileException &cException)
{
cerr <<
"Failed to write to file: "
<< cException.what() << endl;
}
|
What happens if WriteFile() fails and throws a FileException? At this point, we’ve already opened the file, and now control flow jumps to the FileException handler, which prints an error and exits. Note that the file was never closed! This example should be rewritten as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
try
{
OpenFile(strFilename);
WriteFile(strFilename, strData);
CloseFile(strFilename);
}
catch
(FileException &cException)
{
// Make sure file is closed
CloseFile(strFilename);
// Then write error
cerr <<
"Failed to write to file: "
<< cException.what() << endl;
}
|
This kind of error often crops up in another form when dealing with dynamically allocated memory:
1
2
3
4
5
6
7
8
9
10
|
try
{
Person *pJohn =
new
Person(
"John"
, 18, E_MALE);
ProcessPerson(pJohn);
delete
pJohn;
}
catch
(PersonException &cException)
{
cerr <<
"Failed to process person: "
<< cException.what() << endl;
}
|
If ProcessPerson() throws an exception, control flow jumps to the catch handler. As a result, pJohn is never deallocated! This example is a little more tricky than the previous one — because pJohn is local to the try block, it goes out of scope when the try block exits. That means the exception handler can not access pJohn at all (its been destroyed already), so there’s no way for it to deallocate the memory.
However, there are two relatively easy ways to fix this. First, declare pJohn outside of the try block so it does not go out of scope when the try block exits:
1
2
3
4
5
6
7
8
9
10
11
12
|
Person *pJohn = NULL;
try
{
pJohn =
new
Person(
"John"
, 18, E_MALE);
ProcessPerson(pJohn );
delete
pJohn;
}
catch
(PersonException &cException)
{
delete
pJohn;
cerr <<
"Failed to process person: "
<< cException.what() << endl;
}
|
Because pJohn is declared outside the try block, it is accessible both within the try block and the catch handlers. This means the catch handler can do cleanup properly.
The second way is to use a local variable of a class that knows how to cleanup itself when it goes out of scope. The standard library provides a class called std::auto_ptr that can be used for this purpose.std::auto_ptr is a template class that holds a pointer, and deallocates it when it goes out of scope.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include <memory> // for std::auto_ptr
try
{
pJohn =
new
Person(
"John"
, 18, E_MALE);
auto_ptr<Person> pxJohn(pJohn);
// pxJohn now owns pJohn
ProcessPerson(pJohn);
// when pxJohn goes out of scope, it will delete pJohn
}
catch
(PersonException &cException)
{
cerr <<
"Failed to process person: "
<< cException.what() << endl;
}
|
Note that std::auto_ptr should not be set to point to arrays. This is because it uses the delete operator to clean up, not the delete[] operator. In fact, there is no array version of std::auto_ptr! It turns out, there isn’t really a need for one. In the standard library, if you want to do dynamically allocated arrays, you’re supposed to use the std::vector class, which will deallocate itself when it goes out of scope.
Exceptions and destructors
Unlike constructors, where throwing exceptions can be a useful way to indicate that object creation succeeded, exceptions should not be thrown in destructors.
The problem occurs when an exception is thrown from a destructor during the stack unwinding process. If that happens, the compiler is put in a situation where it doesn’t know whether to continue the stack unwinding process or handle the new exception. The end result is that your program will be terminated immediately.
Consequently, the best course of action is just to abstain from using exceptions in destructors altogether. Write a message to a log file instead.
Performance concerns
Exceptions do come with a small performance price to pay. They increase the size of your executable, and they will also cause it to run slower due to the additional checking that has to be performed. However, the main performance penalty for exceptions happens when an exception is actually thrown. In this case, the stack must be unwound and an appropriate exception handler found, which is a relatively an expensive operation. Consequently, exception handling should only be used for truly exceptional cases and catastrophic errors.