Item 12: Understand how throwing an exception differs from passing a parameter or calling a virtual function.
The syntax for declaring function parameters is almost the same as that for catch
class Widget { ... }; // some class; it makes no // difference what it is
void f1(Widget w); // all these functions void f2(Widget& w); // take parameters of void f3(const Widget& w); // type Widget, Widget&, or void f4(Widget *pw); // Widget* void f5(const Widget *pw);
catch (Widget w) ... // all these catch clauses catch (Widget& w) ... // catch exceptions of catch (const Widget& w) ... // type Widget, Widget&, or catch (Widget *pw) ... // Widget* catch (const Widget *pw) ...
You might therefore assume that passing an exception from a throw
site to a catch
clause is basically the same as passing an argument from a function call site to the function's parameter. There are some similarities, to be sure, but there are significant differences,
Let us begin with a similarity. You can pass both function parameters and exceptions by value, by reference, or by pointer. What happens when you pass parameters and exceptions, however, is quite different. This difference grows out of the fact that when you call a function, control eventually returns to the call site (unless the function fails to return), but when you throw an exception, control does not return to the throw
Consider a function that both passes a Widget
as a parameter and throws a Widget
as an
// function to read the value of a Widget from a stream istream operator>>(istream& s, Widget& w);
void passAndThrowWidget() { Widget localWidget;
cin >> localWidget; // pass localWidget to operator>>
throw localWidget; // throw localWidget as an exception }
When localWidget
is passed to operator>>
, no copying is performed. Instead, the reference w
inside operator>>
is bound to localWidget
, and anything done to w
is really done to localWidget
. It's a different story when localWidget
is thrown as an exception. Regardless of whether the exception is caught by value or by reference (it can't be caught by pointer that would be a type mismatch), a copy of localWidget
will be made, and it is the copy that is passed to the catch
clause. This must be the case, because localWidget
will go out of scope once control leaves passAndThrowWidget
, and when localWidget
goes out of scope, its destructor will be called. If localWidget
itself were passed to a catch
clause, the clause would receive a destructed Widget
, an ex-Widget
, a former Widget
, the carcass of what once was but is no longer a Widget
. That would not be useful, and that's why C++ specifies that an object thrown as an exception is always
This copying occurs even if the object being thrown is not in danger of being destroyed. For example, if passAndThrowWidget
declares localWidget
to be
void passAndThrowWidget() { static Widget localWidget; // this is now static; it // will exist until the // end of the program
cin >> localWidget; // this works as before
throw localWidget; // a copy of localWidget is } // still made and thrown
a copy of localWidget
would still be made when the exception was thrown. This means that even if the exception is caught by reference, it is not possible for the catch
block to modify localWidget
; it can only modify a copy of localWidget
. This mandatory copying of exception objects helps explain another difference between parameter passing and throwing an exception: the latter is typically much slower than the former (see Item 15).
When an object is copied for use as an exception, the copying is performed by the object's copy constructor. This copy constructor is the one in the class corresponding to the object's static type, not its dynamic type. For example, consider this slightly modified version of passAndThrowWidget
:
class Widget { ... }; class SpecialWidget: public Widget { ... };
void passAndThrowWidget() { SpecialWidget localSpecialWidget;
...
Widget& rw = localSpecialWidget; // rw refers to a // SpecialWidget
throw rw; // this throws an // exception of type } // Widget!
Here a Widget
exception is thrown, even though rw
refers to a SpecialWidget
. That's because rw
's static type is Widget
, not SpecialWidget
. That rw
actually refers to a SpecialWidget
is of no concern to your compilers; all they care about is rw
's static type. This behavior may not be what you want, but it's consistent with all other cases in which C++ copies objects. Copying is always based on an object's static type (but see Item 25 for a technique that lets you make copies on the basis of an object's dynamic
The fact that exceptions are copies of other objects has an impact on how you propagate exceptions from a catch
block. Consider these two catch
blocks, which at first glance appear to do the same
catch (Widget& w) // catch Widget exceptions { ... // handle the exception
throw; // rethrow the exception so it } // continues to propagate
catch (Widget& w) // catch Widget exceptions { ... // handle the exception
throw w; // propagate a copy of the } // caught exception
The only difference between these blocks is that the first one rethrows the current exception, while the second one throws a new copy of the current exception. Setting aside the performance cost of the additional copy operation, is there a difference between these
There is. The first block rethrows the current exception, regardless of its type. In particular, if the exception originally thrown was of type SpecialWidget
, the first block would propagate a SpecialWidget
exception, even though w
's static type is Widget
. This is because no copy is made when the exception is rethrown. The second catch
block throws a new exception, which will always be of type Widget
, because that's w
's static type. In general, you'll want to use
throw;
syntax to rethrow the current exception, because there's no chance that that will change the type of the exception being propagated. Furthermore, it's more efficient, because there's no need to generate a new exception
(Incidentally, the copy made for an exception is a temporary object. As Item 19 explains, this gives compilers the right to optimize it out of existence. I wouldn't expect your compilers to work that hard, however. Exceptions are supposed to be rare, so it makes little sense for compiler vendors to pour a lot of energy into their
Let us examine the three kinds of catch
clauses that could catch the Widget
exception thrown by passAndThrowWidget
. They
catch (Widget w) ... // catch exception by value
catch (Widget& w) ... // catch exception by // reference
catch (const Widget& w) ... // catch exception by // reference-to-const
Right away we notice another difference between parameter passing and exception propagation. A thrown object (which, as explained above, is always a temporary) may be caught by simple reference; it need not be caught by reference-to-const
. Passing a temporary object to a non-const
reference parameter is not allowed for function calls (see Item 19), but it is for
Let us overlook this difference, however, and return to our examination of copying exception objects. We know that when we pass a function argument by value, we make a copy of the passed object (see Item E22), and we store that copy in a function parameter. The same thing happens when we pass an exception by value. Thus, when we declare a catch
clause like
catch (Widget w) ... // catch by value
we expect to pay for the creation of two copies of the thrown object, one to create the temporary that all exceptions generate, the second to copy that temporary into w
. Similarly, when we catch an exception by
catch (Widget& w) ... // catch by reference catch (const Widget& w) ... // also catch by reference
we still expect to pay for the creation of a copy of the exception: the copy that is the temporary. In contrast, when we pass function parameters by reference, no copying takes place. When throwing an exception, then, we expect to construct (and later destruct) one more copy of the thrown object than if we passed the same object to a
We have not yet discussed throwing exceptions by pointer, but throw by pointer is equivalent to pass by pointer. Either way, a copy of the pointer is passed. About all you need to remember is not to throw a pointer to a local object, because that local object will be destroyed when the exception leaves the local object's scope. The catch
clause would then be initialized with a pointer to an object that had already been destroyed. This is the behavior the mandatory copying rule is designed to
The way in which objects are moved from call or throw
sites to parameters or catch
clauses is one way in which argument passing differs from exception propagation. A second difference lies in what constitutes a type match between caller or thrower and callee or catcher. Consider the sqrt
function from the standard math
double sqrt(double); // from <cmath> or <math.h>
We can determine the square root of an integer like
int i; double sqrtOfi = sqrt(i);
There is nothing surprising here. The language allows implicit conversion from int
to double
, so in the call to sqrt
, i
is silently converted to a double
, and the result of sqrt
corresponds to that double
. (See Item 5 for a fuller discussion of implicit type conversions.) In general, such conversions are not applied when matching exceptions to catch
clauses. In this
void f(int value) { try { if (someFunction()) { // if someFunction() returns throw value; // true, throw an int ... } } catch (double d) { // handle exceptions of ... // type double here } ... }
the int
exception thrown inside the try
block will never be caught by the catch
clause that takes a double
. That clause catches only exceptions that are exactly of type double
; no type conversions are applied. As a result, if the int
exception is to be caught, it will have to be by some other (dynamically enclosing) catch
clause taking an int
or an int&
(possibly modified by const
or volatile
).
Two kinds of conversions are applied when matching exceptions to catch
clauses. The first is inheritance-based conversions. A catch
clause for base class exceptions is allowed to handle exceptions of derived class types, too. For example, consider the diagnostics portion of the hierarchy of exceptions defined by the standard C++ library (see Item E49):
A catch
clause for runtime_error
s can catch exceptions of type range_error
and overflow_error
, too, and a catch
clause accepting an object of the root class exception
can catch any kind of exception derived from this
This inheritance-based exception-conversion rule applies to values, references, and pointers in the usual
catch (runtime_error) ... // can catch errors of type catch (runtime_error&) ... // runtime_error, catch (const runtime_error&) ... // range_error, or // overflow_error
catch (runtime_error*) ... // can catch errors of type catch (const runtime_error*) ... // runtime_error*, // range_error*, or // overflow_error*
The second type of allowed conversion is from a typed to an untyped pointer, so a catch
clause taking a const
void*
pointer will catch an exception of any pointer
catch (const void*) ... // catches any exception // that's a pointer
The final difference between passing a parameter and propagating an exception is that catch
clauses are always tried in the order of their appearance. Hence, it is possible for an exception of a derived class type to be handled by a catch
clause for one of its base class types even when a catch
clause for the derived class is associated with the same try
block! For
try { ... } catch (logic_error& ex) { // this block will catch ... // all logic_error } // exceptions, even those // of derived types catch (invalid_argument& ex) { // this block can never be ... // executed, because all } // invalid_argument // exceptions will be caught // by the clause above
Contrast this behavior with what happens when you call a virtual function. When you call a virtual function, the function invoked is the one in the class closest to the dynamic type of the object invoking the function. You might say that virtual functions employ a "best fit" algorithm, while exception handling follows a "first fit" strategy. Compilers may warn you if a catch
clause for a derived class comes after one for a base class (some issue an error, because such code used to be illegal in C++), but your best course of action is preemptive: never put a catch
clause for a base class before a catch
clause for a derived class. The code above, for example, should be reordered like
try { ... } catch (invalid_argument& ex) { // handle invalid_argument ... // exceptions here } catch (logic_error& ex) { // handle all other ... // logic_errors here }
There are thus three primary ways in which passing an object to a function or using that object to invoke a virtual function differs from throwing the object as an exception. First, exception objects are always copied; when caught by value, they are copied twice. Objects passed to function parameters need not be copied at all. Second, objects thrown as exceptions are subject to fewer forms of type conversion than are objects passed to functions. Finally, catch
clauses are examined in the order in which they appear in the source code, and the first one that can succeed is selected for execution. When an object is used to invoke a virtual function, the function selected is the one that provides the best match for the type of the object, even if it's not the first one listed in the source