Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Exception Safety Considerations
Understand exception dangers and downsides for robust error handling.
Exception dangers and downsides
Like most programming features that provide benefits, exceptions also come with potential downsides. This lesson isn't comprehensive, but highlights major issues to consider when using exceptions or deciding whether to use them.
Cleaning up resources
One of the biggest challenges new programmers face with exceptions is ensuring proper resource cleanup when an exception occurs. Consider this example:
#include <iostream>
try
{
openFile(filename);
writeFile(filename, data);
closeFile(filename);
}
catch (const FileException& exception)
{
std::cerr << "Failed to write to file: " << exception.what() << '\n';
}
What happens if writeFile() throws a FileException? At this point, we've already opened the file, and now control jumps to the exception handler, which prints an error and exits. Notice the file was never closed! This example should be rewritten as follows:
#include <iostream>
try
{
openFile(filename);
writeFile(filename, data);
}
catch (const FileException& exception)
{
std::cerr << "Failed to write to file: " << exception.what() << '\n';
}
// Make sure file is closed regardless of success or failure
closeFile(filename);
By moving the closeFile() call outside the try block, it will execute whether or not an exception was thrown.
This kind of error often appears in another form when dealing with dynamically allocated memory:
#include <iostream>
try
{
auto* parser{ new XmlParser{"config.xml"} };
processConfiguration(parser);
delete parser;
}
catch (const ParsingException& error)
{
std::cerr << "Configuration parsing failed: " << error.what() << '\n';
}
If processConfiguration() throws an exception, control jumps to the catch handler. As a result, parser is never deallocated! This example is trickier than the previous one—because parser is local to the try block, it goes out of scope when the try block exits. The exception handler cannot access parser at all (it's been destroyed already), so there's no way for it to deallocate the memory.
However, there are two relatively straightforward solutions. First, declare parser outside the try block so it doesn't go out of scope when the try block exits:
#include <iostream>
XmlParser* parser{ nullptr };
try
{
parser = new XmlParser{"config.xml"};
processConfiguration(parser);
}
catch (const ParsingException& error)
{
std::cerr << "Configuration parsing failed: " << error.what() << '\n';
}
delete parser;
Because parser is declared outside the try block, it's accessible both within the try block and the catch handlers. This allows proper cleanup.
The second solution is using a local variable of a class that knows how to clean up itself when it goes out of scope (often called a "smart pointer"). The standard library provides std::unique_ptr for this purpose. std::unique_ptr is a template class that holds a pointer and deallocates it when it goes out of scope.
#include <iostream>
#include <memory>
try
{
auto parser{ std::make_unique<XmlParser>("config.xml") };
processConfiguration(parser.get());
// when parser goes out of scope, it automatically deletes the XmlParser
}
catch (const ParsingException& error)
{
std::cerr << "Configuration parsing failed: " << error.what() << '\n';
}
We cover `std::unique_ptr` in lesson 22.5.
The best option (whenever possible) is preferring to stack allocate objects that implement RAII (automatically allocate resources on construction, deallocate resources on destruction). When the object managing the resource goes out of scope for any reason, it will automatically deallocate as appropriate, so we don't have to worry about such things!
Exceptions and destructors
Unlike constructors, where throwing exceptions can be a useful way to indicate object creation failed, exceptions should never be thrown from destructors.
The problem occurs when an exception is thrown from a destructor during the stack unwinding process. If that happens, the compiler faces an impossible decision: should it continue stack unwinding or handle the new exception? The result is immediate program termination.
Consequently, the best approach is avoiding exceptions in destructors entirely. Write error messages to a log file instead.
If an exception is thrown from a destructor during stack unwinding, the program will be terminated.
Performance concerns
Exceptions do impose a small performance cost. They increase executable size and may cause programs to run slightly slower due to additional checking. However, the main performance penalty occurs when an exception is actually thrown. In this case, the stack must be unwound and an appropriate exception handler found, which is a relatively expensive operation.
As a note, some modern computer architectures support an exception model called zero-cost exceptions. Zero-cost exceptions, when supported, have no additional runtime cost in the non-error case (which is the case we care most about for performance). However, they incur an even larger penalty when an exception is actually found.
So when should I use exceptions?
Exception handling is best used when all of the following are true:
- The error being handled is likely to occur only infrequently.
- The error is serious and execution could not continue otherwise.
- The error cannot be handled at the place where it occurs.
- There isn't a good alternative way to return an error code back to the caller.
As an example, consider a function that expects the user to pass in the name of a configuration file on disk. Your function will open this file, read some settings, close the file, and return the results to the caller. Now suppose the user passes in a filename that doesn't exist, or an empty string. Is this a good candidate for an exception?
In this case, the first two criteria are satisfied—this won't happen often, and your function can't produce results when it doesn't have valid input data. The function can't handle the error itself—it's not the function's job to re-prompt the user for a new filename, and that might not even be appropriate depending on how your program is designed. The fourth criterion is key—is there a good alternative way to return an error code to the caller? It depends on the details of your program. If you can return a status enum or a std::optional to indicate failure, that's probably better. If not, then an exception would be reasonable.
Summary
Resource cleanup challenges: One of the biggest challenges with exceptions is ensuring proper resource cleanup when an exception occurs. Resources allocated before an exception is thrown must be properly released.
RAII solves cleanup: The best approach is using RAII (Resource Acquisition Is Initialization) by placing resource management inside member objects that automatically clean up when they go out of scope, even during exceptional circumstances.
Smart pointers for dynamic memory: Use std::unique_ptr and other smart pointers instead of raw pointers to ensure automatic memory deallocation when exceptions occur.
Destructors and exceptions: Never throw exceptions from destructors. If an exception is thrown from a destructor during stack unwinding, the program will be terminated immediately.
Performance concerns: Exceptions impose a small performance cost through increased executable size and may cause programs to run slightly slower. The main penalty occurs when an exception is actually thrown, requiring stack unwinding and handler location.
Zero-cost exceptions: Some modern architectures support zero-cost exceptions that have no runtime cost in the non-error case but incur a larger penalty when an exception is found.
When to use exceptions: Exception handling is best used when the error is infrequent, serious, cannot be handled locally, and there's no good alternative way to return an error code to the caller.
Understanding the downsides and limitations of exceptions helps you make informed decisions about when exception handling is appropriate versus when alternative error handling mechanisms would be better suited.
Exception Safety Considerations - Quiz
Test your understanding of the lesson.
Practice Exercises
Exception Safety with RAII
Demonstrate exception safety by using RAII to manage resources. Show how resources are properly cleaned up even when exceptions are thrown.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!