Lesson 13 - Exceptions
In modern C++, in most scenarios, the preferred way to report and handle both logic errors and runtime errors is to use exceptions. This is especially true when the stack might contain several function calls between the function that detects the error and the function that has the context to know how to handle it. Exceptions provide a formal, well-defined way for code that detects errors to pass the information up the call stack.
Exceptions are preferred in modern C++ for the following reasons:
- An exception forces calling code to recognise an error condition and handle it. Unhandled exceptions stop program execution.
- An exception jumps to the point in the call stack that can handle the error. Intermediate functions can let the exception propagate. They do not have to coordinate with other layers.
- The exception stack-unwinding mechanism destroys all objects in scope according to well-defined rules after an exception is thrown.
- An exception enables a clean separation between the code that detects the error and the code that handles the error.
To catch exceptions, a portion of code is placed under exception inspection. This is done by enclosing that portion of code in a try-block
. When an exceptional circumstance arises within that block, an exception is thrown that transfers the control to the exception handler. If no exception is thrown, the code continues normally and all handlers are ignored.
#include <stdexcept>
#include <limits>
#include <iostream>
class MyClass
{
public:
void MyFunc(char c)
{
if(c < std::numeric_limits<char>::max())
throw std::invalid_argument("MyFunc argument too large.");
//...
}
};
int main()
{
try
{
MyFunc(256); //cause an exception to throw
}
catch(const std::invalid_argument& e)
{
std::cerr << e.what() << std::endl;
return -1;
}
//...
return 0;
}
In the try
block, if an exception is thrown it will be caught by the first associated catch block whose type matches that of the exception. In other words, execution jumps from the throw
statement to the catch statement. If no usable catch
block is found, std::terminate
is invoked and the program exits. In C++, any type may be thrown; however, it is recommended that you throw a type that derives directly or indirectly from std::exception
. In the previous example, the exception type, std::invalid_argument
, is defined in the standard library in the stdexcept
header file. C++ does not provide, and does not require, a finally
block to make sure that all resources are released if an exception is thrown. The resource acquisition is initialisation (RAII) idiom, which uses smart pointers, provides the required functionality for resource cleanup.
Multiple handlers (i.e., catch expressions) can be chained; each one with a different parameter type. Only the handler whose argument type matches the type of the exception specified in the throw statement is executed.
If an ellipsis (…
) is used as the parameter of catch, that handler will catch any exception no matter what the type of the exception thrown. This can be used as a default handler that catches all exceptions not caught by other handlers.
try
{
// code here
}
catch (int param) { cout << "int exception"; }
catch (char param) { cout << "char exception"; }
catch (...) { cout << "default exception"; }
Basic guidelines
Robust error handling is challenging in any programming language. Although exceptions provide several features that support good error handling, they cannot do all the work for you. To realise the benefits of the exception mechanism, keep exceptions in mind as you design your code.
- Use asserts to check for errors that should never occur. Use exceptions to check for errors that might occur, for example, errors in input validation on parameters of public functions. For more information, see the section titled Exceptions vs. Assertions.
- Use exceptions when the code that handles the error might be separated from the code that detects the error by one or more intervening function calls. Consider whether to use error codes instead in performance-critical loops when code that handles the error is tightly-coupled to the code that detects it.
- For every function that might throw or propagate an exception, provide one of the three exception guarantees: the strong guarantee, the basic guarantee, or the nothrow (noexcept) guarantee. ** Throw exceptions by value, catch them by reference. Don’t catch what you can't handle.
- Do not use exception specifications, which are deprecated in C++11.
- Use standard library exception types when they apply. Derive custom exception types from the exception Class hierarchy.
- Do not allow exceptions to escape from destructors or memory-deallocation functions.
Exceptions and performance
The exception mechanism has a very minimal performance cost if no exception is thrown. If an exception is thrown, the cost of the stack traversal and unwinding is roughly comparable to the cost of a function call. Additional data structures are required to track the call stack after a try block is entered, and additional instructions are required to unwind the stack if an exception is thrown. However, in most scenarios, the cost in performance and memory footprint is not significant. The adverse effect of exceptions on performance is likely to be significant only on very memory-constrained systems, or in performance-critical loops where an error is likely to occur regularly and the code to handle it is tightly coupled to the code that reports it. In any case, it's impossible to know the actual cost of exceptions without profiling and measuring. Even in those rare cases when the cost is significant, you can weigh it against the increased correctness, easier maintainability, and other advantages that are provided by a well-designed exception policy.