C++ Core Guidelines: Rules to Exception Handling
This is a cross-post from www.ModernesCpp.com.
Today's post is about the right way to throw and catch exceptions. This means in particular when you should throw and how you should catch an exception.
Here are the rules for today:
- E.14: Use purpose-designed user-defined types as exceptions (not built-in types)
- E.15: Catch exceptions from a hierarchy by reference
- E.16: Destructors, deallocation, and swap must never fail
- E.17: Don’t try to catch every exception in every function
- E.18: Minimize the use of explicit try/catch
Let me directly jump into the first one.
E.14: Use purpose-designed user-defined types as exceptions (not built-in types)
You should not use standard exceptions types or even built-in types as an exception. Here are the two don't from the guidelines:
A built-in type
void my_code() // Don't
{
// ...throw 7; // 7 means "moon in the 4th quarter"https:// ...
}
void your_code() // Don't
{
try {
// ...
my_code();
// ...
}
catch(int i) { // i == 7 means "input buffer too small"https:// ...
}
}
In this case, the exception is just an int without any semantic. What 7 means stand in the comment, but should better be a self-describing type. The comment can be wrong. To be sure, you have to look up in the documentation to get an idea. You can not attach any meaningful information to an exception of kind int. If you have a 7, I assume, you use at least the numbers 1 to 6 for your exception handling. 1 meaning an unspecific error and so on. This is way too sophisticated, error-prone, and quite hard to read and to maintain.
A standard exception
void my_code() // Don't
{
// ...throw runtime_error{"moon in the 4th quarter"};
// ...
}
void your_code() // Don't
{
try {
// ...
my_code();
// ...
}
catch(const runtime_error&) { // runtime_error means "input buffer too small"https:// ...
}
}
Using a standard exception instead of a built-in type is better because you can attach additional information to an exception or build hierarchies of exceptions. This is better but not good. Why? The exception is too generic. It's just a runtime_error. Image the function my_code is part of an input sub-system. If the caller of the function catches the exception by std::runtime_error, he has no idea if it was a generic error such as "input buffer too small" or a sub-system specific error such as "input device is not connected".
To overcome these issues derive your specific exception from std: . Here is a short example to give you the idea:
class InputSubSystemException: public std::exception{
const char* what() const noexcept override {
return "Provide more details to the exception";
}
};
Now, the client of the input sub-system can specifically catch the exception via catch(const InputSubSystemException& ex). Additionally, you can refine the exception hierarchy by further deriving from the class InputSubSystemException.
E.15: Catch exceptions from a hierarchy by reference
If you catch an exception from a hierarchy by-value, you may become a victim of slicing.
Imagine, you derive from InputSubSystemException (rule E.14) a new exception class USBInputException and catch the exception by-value of type InputSubSystemException. Now, an exception of type USBInputException is thrown.
void subSystem(){
// ...throw USBInputException();
// ...
}
void clientCode(){
try{
subSystem();
}
catch(InputSubSystemException e) { // slicing may happen// ...
}
}
By catching the USBInputException by-value to InputSubSystemException, slicing kicks in and e has the simpler type InputSubSystemException. Read the details of slicing in my previous post: C++ Core Guidelines: Rules about Don'ts.
To say it explicitly:
- Catch your exception by const reference and only by reference if you want to modify the exception.
- If you rethrow an exception e in the exception handler, just use throw and not throw e. In the second case, e would be copied.
E.16: Destructors, deallocation, and swap must never fail
This rule is quite obvious. Destructors and deallocations should never throw because their no reliable way to handle an exception during the destruction of an object.
swap is often used as a basic building block for implementing copy and move semantic for a type. If an exception happens during swap you are, therefore, left with a non-initialised or not fully initialised . Read more about the swap here: C++ Core Guidelines: Comparison, Swap, and Hash.
The next two rules to the adequate usage of try and except are quite similar.
E.17: Don’t try to catch every exception in every function and E.18: Minimize the use of explicit try/catch
From the control flow perspective, try/catch has a lot in common with the goto statement. This means if an exception is thrown, the control flow directly jumps to the exception handler which is maybe in a totally different function of even sub-system. At the end you may get spaghetti code; meaning code that has a difficult to predict and to maintain control flow.
In the end, we are back to rule E.1: Develop an error-handling strategy early in a design.
Now, the question is: How should you structure your exception handling? I think you should ask yourself the question: Is it possible to handle the exception locally? If yes, do it. If no, let the exception propagate until you can sufficiently handle it. Often sub-system boundaries are the appropriate place to handle exceptions because you want to protect the client of the sub-system for arbitrary exceptions. At the boundary level, you have the interface consisting of the regularly and irregularly control flow. The regular communication is the functional aspect of the interface or what the system should do. The irregular communication stands for the non-functional aspects or how the system should perform. A big part of the non-functional aspects is the exception-handling and, therefore, the right place to handle the propagated exceptions.
What's next?
Six rules to error handling are still left in the C++ core guidelines. They are the topic for the next post before I go on with the rules to constants and immutability.
Thanks a lot to my Patreon Supporters: Eric Pederson, Paul Baxter, Meeting C++, Matt Braun, Avi Lachmish, Roman Postanciuc, and Venkata Ramesh Gudpati.
Get your e-book at :
The C++ Standard Library
Concurrency With Modern C++
Get Both as one Bundle
With C++11, C++14, and C++17 we got a lot of new C++ libraries. In addition, the existing ones are greatly improved. The key idea of my book is to give you the necessary information to the current C++ libraries in about 200 pages. C++11 is the first C++ standard that deals with concurrency. The story goes on with C++17 and will continue with C++20.
I'll give you a detailed insight the current and the upcoming concurrency in C++. This insight includes the theory and a lot of practice with more the 100 source files.
Get my books "The C++ Standard Library" (including C++17) and "Concurrency with Modern C++" in a bundle.
In sum, you get more than 600 pages full of modern C++ and more than 100 source files presenting concurrency in practice.