Reading Assignment: All of Programming Chapter 19 Error Handling and Exceptions
The problem may come from a variety of sources: the user of the program, a logical error in the program itself, a poblem with the computer’s hardware, an unplugged network cable, or any number of other issues.
The worst possible way to deal with any problem is for the program to produce the wrong answer without informing the user of the problem-a silent failure. A course of action which is slightly better in most cases is to abort the program and inform the user of the problem.
Bullet proof code: code which gracefully handle any problem that can happen.
C-Style Error Handling
In C, error handling involves checking the return value of a function that you call, and possibly returning an error to your function’s caller as needed.
C++-Style: Exceptions
There are three key characteristics we desire in an error handling mechanism.
- We want to remove the possibility that an error can be silently ignored.
- We would like error handling to be as unobtrusive as possible in our code-reducing the “cluttered” feeling given by C-style error handling.
- The ability to convey extra information about the error.
To achieve all of these goals, C++ introduces exceptions, which involve two concepts: throw and try/catch. A programmer places code where an error might occur in a try block. The try block is immediately followed by one or more catch blocks. Each catch block specifies how to handle a particular type of exception. Code that detects an error which it cannot handle throws an exception to indicate the problem.
Executing Code with Exceptions
Throwing an exception is accomplished by executing a throw statement, which is the keyword throw
, followed by an expression which evaluates to the exception to throw. For example:
1 | throw std::excetion(); |
In C++, we can throw any type of object, but should generally only throw subtypes of std::excetion
. To use std::excetion
, you should #include <exception>
. Thre are also a variety of built-in subtypes of std::excetion
, which typically require you to #include <stdexcept>
You can read about them in the documentation for std::excetion
.
1 | try { |
When an exception is thrown, the following steps occur:
- The exception object is potentially copied out of the frame into some location that will persist through handling.
- If the execution arrow is currently inside of a try block, then it jumps to the close curly brace of the try block, and begins trying to match the exception type against the handler type in the order they appear.
- If the execution arrow was not currently inside of a try block in Step 2, then the exception propagates out of the function that is in.
- Once an exception is caught, it is handled by the code within the
catch
block. - If/When the exection arrow reaches the close curly brace of the handler, then the program is finished handling the exception. The exception object is deallocated, and execution continues normally at the statement immediately following the close curly brace.
There are slight variations on the try and catch blocks that are worth mentioning. For the catch block, one can specify that it will catch any type, but in doing so, it cannot bind the exception to a variable. This generic catch is accomplished by placing three dots in the parenthesis where one would normally write the type of exception to catch the name to bind it to. That is, we can write:
1 | try { |
For a try block, there is a variation called a function try block, which is primarily of use in a constructor, where the programmer wants to catch exceptions that may occur in the initializer list. In a function try block, the keyword try appears before the function body and the handlers appear after the close curly brace of the function’s body. For example, we might write:
1 | class X { |
Exceptions as Part of a Function’s Interface
Functions may include an exception specification-a list of the types of exception that it may throw.
1 | int f(int x) throw(std::bad_alloc, std::invalid_argument); |
The first function f
is declared to throw two possible exception types, std::bad_alloc
and std::invalid_argument
. The second function is declared to throw no exceptions. The third does not include an exception specification, so it may throw any type of exception.
When overriding a method which provides an exception specification, overriding method must have an exception specification which is the same, or more restrictive than the exception specification of the inherited method.
1 | // Option 1: same as the parent class |
Exception Corner Cases
unexpected()
is called when a function throws an exception that is not allowed by its exception that is not allowed by its exception specification. The default behavior of unexpected()
depends on whether the exception specification allows std::bad_exception
. If so, then the unexpected()
function throws a std::bad_exception
, and the exception handling continues normally. If std::bad_exception
is not permitted, then unexpected()
calls terminate()
.
Using Exceptions Properly
The guidelines below are here to help you understand how and when to use exceptions.
- Exceptions are for error conditions.
- Throw an unnamed temporary, created locally.
- Re-throw an exception only with throw.
- Catch by (possibly constant) reference.
- Declare handlers from most to least specific.
- Desctructors should never throw exceptions.
- Exception types should inherit from
std::exception
. - Keep exception types simple.
- Override the
what()
method in your own exception types. - Be aware of the exception behavior of all code you work with.
Exception Safety
At a minimum, professional code should provide basic exception guarantees-it should ensure that if an exception happens, the objects invariants are maintained, and no memory is leaked.
Resource Acquisition Is Initialization
We can use the fact that destructors are invoked on objects in the frame when the exception destroys the frame to simplify exception safe resource management(whether memory allocation/deallocation, or other resources) in the general case. What we need is an object in the local frame that is constructed when the resource is allocated, and whose destructor frees that resource(unless we explicitly remove the resource from that object’s control). This design principles is called Resource Acquisition is Initialization(or RAII for short).
C++’s STL provides a templated class-std::auto_ptr<T>
which is designed to help write exception safe code with the RAII paradigm. The basic use of the std::auto_ptr<T>
template to initialize it by passing the result of new to its constructor, then make use of the get
to obtain the underlying pointer, or the overloaded *
operator to dereference that pointer. If the auto_ptr
is destroyed while it still owns the pointer, it will delete the pointer it owns. You can release the pointer from its ownership with the release
method, after which it will not destroy it(and get
will return NULL
). For example(suppose A, B, X and Y are classes):
1 | X * someFunction(A & anA, B & aB){ |
Exception Safety Idiom: Temp-and-Swap
One exception safety idiom is to modify an object by creating a temporary object of the same type, and then swapping the contents of the newly created temporary object with the original object. The idiom provide a strong exception guarantee. In this idiom, our code might look generally like this:
1 | class SomeClass{ |
std::swap
is a templated function which performs the swap operation using the copy constructor and copy assignmnent operator of the class involved. Accordingly, you cannot use the general std::swap
to implement move assignmnent operator. We can, however, define our own swap operation, and if we wanted to, provide it as an explicit specialization of the std::swap
template.