1. Exception Handling
Exception handling relies on the problem-detecting part throwing an object to a handler. The type and contents of that object allow the two parts to communicate about what went wrong.Sales_item operator+(const Sales_item& lhs, const Sales_item& rhs)
{
if (!lhs.same_isbn(rhs))
throw runtime_error("Data must refer to same ISBN");
Sales_item ret(lhs);
return
ret += rhs;
return ret;
}
Sales_item item1, item2, sum;
while (cin >> item1 >> item2) {
try {
sum = item1 + item2;
} catch (const runtime_error &e) {
cerr << e.what() << " Try again.\n"
<< endl;
}
}
2. Throwing an Exception of Class Type
An exception is raised by throwing an object. The type of that object determines which handler will be invoked. The selected handler is the one nearest in the call chain that matches the type of the object.
Exceptions are thrown and caught in ways that are similar to how arguments are passed to functions. An exception can be an object of any type that can be passed to a nonreference parameter, meaning that it must be possible to copy objects of that type. Recall that when we pass an argument of array or function type, that argument is automatically converted to an pointer. The same automatic conversion happens for objects that are thrown. As a consequence, there are no exceptions of array or function types. Instead, if we throw an array, the thrown object is converted to a pointer to the first element in the array. Similarly, if we throw a function, the function is converted to a pointer to the function. The fact that control passes from one location to another has two important implications:
- Functions along the call chain are prematurely exited. Following section discusses what happens when functions are exited due to an exception.
- In general, the storage that is local to a block that throws an exception is not around when the exception is handled.
Because local storage is freed while handling an exception, the object that is thrown is not stored locally. Instead, the throw expression is used to initialize a special object referred to as the exception object . The exception object is managed by the compiler and is guaranteed to reside in space that will be accessible to whatever catch is invoked. This object is created by a
throw , and is initialized as a copy of the expression that is thrown. The exception object is passed to the corresponding catch and is destroyed after the exception is completely handled. The exception object is created by copying the result of the thrown
expression; that result must be of a type that can be copied. What’s important to know is how the form of the throw expression interacts with types related by inheritance. When an exception is thrown, the static, compile-time type of the thrown object determines the type of the exception object.
The one case where it matters that a throw expression throws the static type is if we dereference a pointer in a throw. The result of dereferencing a pointer is an object whose type matches the type of the pointer. If the pointer points to a type from an inheritance hierarchy, it is possible that the type of the object to which the pointer points is different from the type of
the pointer. Regardless of the object’s actual type, the type of the exception object matches the static type of the pointer. If that pointer is a base-class type pointer that points to a derived-type object, then that object is sliced down; only the base-class part is thrown.
In particular, it is always an error to throw a pointer to a local object for the same reasons as it is an error to return a pointer to a local object from a function. It is usually a bad idea to tHRow a pointer: Throwing a pointer requires that the object to which the pointer points exist wherever the corresponding handler resides.
3. Stack Unwinding
When an exception is thrown, execution of the current function is suspended and the search begins for a matching catch clause. The search starts by checking whether the tHRow itself is located inside a try block. If so, the catch clauses associated with that try are examined to see if one of them matches the thrown object. If a matching catch is found, the exception is
handled. If no catch is found, the current function is exitedits memory is freed and local objects are destroyed and the search continues in the calling function.
If the call to the function that threw is in a try block, then the catch clauses associated with that try are examined. If a matching catch is found, the exception is handled. If no matching catch is found, the calling function is also exited, and the search continues in the function that called this one.
This process, known as stack unwinding, continues up the chain of nested function calls until a catch clause for the exception is found. As soon as a catch clause that can handle the exception is found, that catch is entered, and execution continues within this handler. When the catch completes, execution continues at the point immediately after the last catch clause associated with that try block.
When a function is exited due to an exception, the compiler guarantees that the local objects are properly destroyed. During stack unwinding, the memory used by local objects is freed and destructors for local objects of class type are run.
Destructors are often executed during stack unwinding. When destructors are executing, the exception has been raised but not yet handled. While stack unwinding is in progress for an exception, a destructor that throws another exception of its own that it does not also handle, causes the library terminate function is called. Ordinarily, terminate calls abort , forcing an abnormal exit from the entire program. Because terminate ends the program, it is usually a very bad idea for a destructor to do anything that might cause an exception. The standard library types all guarantee that their destructors will not raise an exception.
Unlike destructors, it is often the case that something done inside a constructor might throw an exception. If an exception occurs while constructing an object, then the object might be only partially constructed. Some of its members might have been initialized, and others might not have been initialized before the exception occurs. Even if the object is only partially constructed, we are guaranteed that the constructed members will be properly destroyed. Similarly, an exception might occur when initializing the elements of an array or other container type. Again, we are guaranteed that the constructed elements will be destroyed.
uncaught exceptions terminate the program.
3. Catching an Exception
The exception specifier in a catch clause looks like a parameter list that contains exactly one parameter. The exception specifier is a type name followed by an optional parameter name. During the search for a matching catch , the catch that is found is not necessarily the one that matches the exception best. Instead, the catch that is selected is the first catch found that can
handle the exception. As a consequence, in a list of catch clauses, the most specialized catch must appear first. Most conversions are not allowed the types of the exception and the catch specifier must match exactly with only a few possible differences:
- Conversions from non const to const are allowed. That is, a throw of a non const object can match a catch specified to take a const reference.
- Conversions from derived type to base type are allowed.
- An array is converted to a pointer to the type of the array; a function is converted to the appropriate pointer to function type.
In particular, neither the standard arithmetic conversions nor conversions defined for class types are permitted.
When a catch is entered, the catch parameter is initialized from the exception object. As with a function parameter, the exception-specifier type might be a reference. The exception object itself is a copy of the object that was thrown. Whether the exception object is copied again into the catch site depends on the exception-specifier type. Like a parameter declaration, an exception specifier for a base class can be used to catch an exception object of a derived type. Usually, a catch clause that handles an exception of a type related by inheritance ought to define its parameter as a reference. Because objects (as opposed to references) are not polymorphic.
Because catch clauses are matched in the order in which they appear, programs that use exceptions from an inheritance hierarchy must order their catch clauses so that handlers for a derived type occurs before a catch for its base type.
4. Rethrow
A catch can pass the exception out to another catch further up the list of function calls by rethrowing the exception. A rethrow is a throw that is not followed by a type or an expression: throw; An empty throw can appear only in a catch or in a function called (directly or indirectly) from a catch. If an empty throw is encountered when a handler is not active, terminate is called.
The exception that is thrown is the original exception object, not the catch parameter. When a catch parameter is a base type, then we cannot know the actual type thrown by a rethrow expression. That type depends on the dynamic type of the exception object, not the static type of the catch parameter. In general, a catch might change its parameter. If, after changing its parameter, the catch rethrows the exception, then those changes will be propagated only if the exception specifier is a reference:catch (my_error &eObj) { // specifier is a reference type
eObj.status = severeErr; // modifies the exception object
throw; // the status member of the exception object is severeErr
} catch (other_error eObj) { // specifier is a nonreference type
eObj.status = badErr; // modifies local copy only
throw; // the status member of the exception rethrown is unchanged
}
5. The Catch-All Handler
A catch(…) is often used in combination with a rethrow expression. The catch does whatever local work can be done and then rethrows the exception:void manip() {
try {
// actions that cause an exception to be thrown
}
catch (...) {
// work to partially handle the exception
throw;
}
}
6. Function Try Blocks and Constructors
Constructor initializers are processed before the constructor body is entered. A catch clause inside the constructor body cannot handle an exception that might occur while processing a constructor initializer. To handle an exception from a constructor initializer, we must write the constructor as a function try block.template <class T> Handle<T>::Handle(T *p)
try : ptr(p), use(new size_t(1))
{
// empty function body
}
catch(const std::bad_alloc &e)
{
handle_out_of_memory(e);
}