|
Section 17:
|
[17.6] Exception handling seems to make my life more difficult; clearly I'm not the problem, am I??
Absolutely you might be the problem!
The C++ exception handling mechanism can be powerful and useful, but if you
use it with the wrong mindset, the result can be a mess. If you're getting
bad results, for instance, if your code seems unnecessarily convoluted or
overly cluttered with try blocks, you might be suffering from a "wrong
mindset." This FAQ gives you a list of some of those wrong mindsets.
Warning: do not be simplistic about these "wrong mindsets." They are
guidelines and ways of thinking, not hard and fast rules. Sometimes you will
do the exact opposite of what they recommend — do not write me about
some situation that is an exception (no pun intended) to one or more of them
— I guarantee that there are exceptions. That's not the point.
Here are some "wrong exception-handling mindsets" in no apparent order:
- The return-codes mindset: This causes programmers to clutter their
code with gobs of try blocks. Basically they think of a throw
as a glorified return code, and a try/catch as a glorified "if
the return code indicates an error" test, and they put one of these
try blocks around just about every function that can
throw.
- The Java mindset: In Java, non-memory resources are reclaimed via
explicit try/finally blocks. When this mindset is used in
C++, it results in a large number of unnecessary try blocks, which,
compared with RAII, clutters the code and makes the logic harder to follow.
Essentially the code swaps back and forth between the "good path" and the "bad
path" (the latter meaning the path taken during an exception). With RAII, the
code is mostly optimistic — it's all the "good path," and the cleanup code
is buried in destructors of the resource-owning objects. This also helps
reduce the cost of code reviews and unit-testing, since these "resource-owning
objects" can be validated in isolation (with explicit
try/catch blocks, each copy must be unit-tested and inspected
individually; they cannot be handled as a group).
- Organizing the exception classes around the physical thrower rather
than the logical reason for the throw: For example, in a banking app,
suppose any of five subsystems might throw an exception when the customer has
insufficient funds. The right approach is to throw an exception representing
the reason for the throw, e.g., an "insufficient funds exception"; the
wrong mindset is for each subsystem to throw a subsystem-specific exception.
For example, the Foo subsystem might throw objects of class
FooException, the Bar subsystem might throw objects of class
BarException, etc. This often leads to extra
try/catch blocks, e.g., to catch a FooException,
repackage it into a BarException, then throw the latter. In general,
exception classes should represent the problem, not the chunk of code that
noticed the problem.
- Using the bits / data within an exception object to differentiate
different categories of errors: Suppose the Foo subsystem in our
banking app throws exceptions for bad account numbers, for attempting to
liquidate an illiquid asset, and for insufficient funds. When these three
logically distinct kinds of errors are represented by the same exception
class, the catchers need to say if to figure out what the problem
really was. If your code wants to handle only bad account numbers, you need
to catch the master exception class, then use if to determine
whether it is one you really want to handle, and if not, to rethrow it. In
general, the preferred approach is for the error condition's logical category
to get encoded into the type of the exception object, not into the
data of the exception object.
- Designing exception classes on a subsystem by subsystem basis: In
the bad old days, the specific meaning of any given return-code was local to a
given function or API. Just because one function uses the return-code of 3 to
mean "success," it was still perfectly acceptable for another function to use
3 to mean something entirely different, e.g., "failed due to out of memory."
Consistency has always been preferred, but often that didn't happen
because it didn't need to happen. People coming with that mentality
often treat C++ exception-handling the same way: they assume exception classes
can be localized to a subsystem. That causes no end of grief, e.g., lots of
extra try blocks to catch then throw a repackaged
variant of the same exception. In large systems, exception hierarchies
must be designed with a system-wide mindset. Exception classes cross
subsystem boundaries — they are part of the intellectual glue that holds the
architecture together.
- Use of raw (as opposed to smart) pointers: This is actually just a
special case of non-RAII coding, but I'm calling it out because it is so
common. The result of using raw pointers is, as above, lots of extra
try/catch blocks whose only purpose in life is to
delete an object then re-throw the exception.
- Confusing logical errors with runtime situations: For example,
suppose you have a function f(Foo* p) that must never be called with
the NULL pointer. However you discover that somebody somewhere is sometimes
passing a NULL pointer anyway. There are two possibilities: either they are
passing NULL because they got bad data from an external user (for example, the
user forgot to fill in a field and that ultimately resulted in a NULL pointer)
or they just plain made a mistake in their own code. In the former case, you
should throw an exception since it is a runtime situation (i.e., something you
can't detect by a careful code-review; it is not a bug). In the latter case,
you should definitely fix the bug in the caller's code. You can still add
some code to write a message in the log-file if it ever happens again, and you
can even throw an exception if it ever happens again, but you must not merely
change the code within f(Foo* p); you must, must, MUST
fix the code in the caller(s) of f(Foo* p).
There are other "wrong exception-handling mindsets," but hopefully those will
help you out. And remember: don't take those as hard and fast rules. They
are guidelines, and there are exceptions to each.
|