Lab 8 - Exceptions
Lab 8 - Exceptions
What good can using exceptions do for me? The basic answer is: Using exceptions for error
handling makes your code simpler, cleaner, and less likely to miss errors. But whats wrong
with good old errno and if-statements? The basic answer is: Using those, your error
handling and your normal code are closely intertwined. That way, your code gets messy and it
becomes hard to ensure that you have dealt with all errors (think spaghetti code or a rats
nest of tests).
First of all there are things that just cant be done right without exceptions. Consider an error
detected in a constructor; how do you report the error? You throw an exception. Thats the
basis of RAII(Resource Acquisition Is Initialization), which is the basis of some of the most
effective modern C++ design techniques: A constructors job is to establish the invariants for
the class (create the environment in which the member functions are to run) and that often
requires the acquisition of resources, such as memory, locks, files, sockets, etc.
Imagine that we did not have exceptions, how would you deal with an error detected in a
constructor? Remember that constructors are often invoked to initialize/construct objects in
variables:
1.
2.
The vector or ofstream (output file stream) constructor could either set the variable into a
bad state (as ifstream does by default) so that every subsequent operation fails. Thats not
ideal. For example, in the case of ofstream, your output simply disappears if you forget to
check that the open operation succeeded. For most classes that results are worse. At least, we
would have to write:
1.
2.
3.
4.
Thats an extra test per object (to write, to remember or forget). This gets really messy for
classes composed of several objects, especially if those sub-objects depend on each other. For
more information see The C++ Programming Language section 8.3, Chapter 14, and Appendix
E or the (more academic) paper Exception safety: Concepts and techniques.
So writing constructors can be tricky without exceptions, but what about plain old functions?
We can either return an error code or set a non-local variable (e.g., errno). Setting a global
variable doesnt work too well unless you test it immediately (or some other function might
have re-set it). Dont even think of that technique if you might have multiple threads accessing
the global variable. The trouble with return values are that choosing the error return value can
require cleverness and can be impossible:
1.
2.
3.
There is no possible value for my_negate() to return: Every possible int is the correct answer
for some int and there is no correct answer for the most negative number in the twoscomplement representation. In such cases, we would need to return pairs of values (and as
usual remember to test) See Stroustrups Beginning programming book for more examples and
explanations.
Common objections to the use of exceptions:
But exceptions are expensive! Not really. Modern C++ implementations reduce the overhead of
using exceptions to a few percent (say, 3%) and thats compared to no error handling. Writing code
with error-return codes and tests is not free either. As a rule of thumb, exception handling is
extremely cheap when you dont throw an exception. It costs nothing on some implementations. All
the cost is incurred when you throw an exception: that is, normal code is faster than code using
error-return codes and tests. You incur cost only when you have an error.
But in JSF++ Stroustrup himself bans exceptions outright! JSF++ is for hard-real time and safetycritical applications (flight control software). If a computation takes too long someone may die. For
that reason, we have to guarantee response times, and we cant with the current level of tool
support do that for exceptions. In that context, even free store allocation is banned! Actually, the
JSF++ recommendations for error handling simulate the use of exceptions in anticipation of the day
where we have the tools to do things right, i.e. using exceptions.
But throwing an exception from a constructor invoked by new causes a memory leak! Nonsense!
Thats an old-wives tale caused by a bug in one compiler and that bug was immediately fixed over
a decade ago.
In C++, exceptions are used to signal errors that cannot be handled locally, such as the failure
to acquire a resource in a constructor. For example:
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
class VectorInSpecialMemory {
int sz;
int* elem;
public:
VectorInSpecialMemory(int s)
: sz(s)
, elem(AllocateInSpecialMemory(s))
{
if (elem == nullptr)
throw std::bad_alloc();
}
...
};
Do not use exceptions as simply another way to return a value from a function. Most users
assume as the language definition encourages them to that ** exception-handling code is
error-handling code**, and implementations are optimized to reflect that assumption.
A key technique is resource acquisition is initialization (sometimes abbreviated to RAII), which
uses classes with destructors to impose order on resource management. For example:
1.
2.
3.
4.
5.
void fct(string s)
{
File_handle f(s,"r"); // File_handle's constructor opens the file called "s"
// use f
} // here File_handle's destructor closes the file
If the use f part of fct() throws an exception, the destructor is still invoked and the file is
properly closed. This contrasts to the common unsafe usage:
1.
2.
3.
4.
5.
6.
If the use f part of old_fct throws an exception or simply does a return the file isnt
closed. In C programs, longjmp() is an additional hazard.
Use throw only to signal an error (which means specifically that the function couldnt do what it
advertised, and establish its postconditions).
Use catch only to specify error handling actions when you know you can handle an error (possibly
by translating it to another type and rethrowing an exception of that type, such as catching
a bad_alloc and rethrowing a no_space_for_file_buffers).
Do not use throw to indicate a coding error in usage of a function. Use assert or other mechanism to
either send the process into a debugger or to crash the process and collect the crash dump for the
developer to debug.
Do not use throw if you discover unexpected violation of an invariant of your component, use assert
or other mechanism to terminate the program. Throwing an exception will not cure memory
corruption and may lead to further corruption of important user data.
There are other uses of exceptions popular in other languages but not idiomatic in C++
and deliberately not supported well by C++ implementations (those implementations are
optimized based on the assumption that exceptions are used for error handling).
In particular, do not use exceptions for control flow. throw is not simply an alternative way of
returning a value from a function (similar to return). Doing so will be slow and will confuse
most C++ programmers who are rightly used to seeing exceptions used only for error handling.
Similarly, throw is not a good way of getting out of a loop.
What are some ways try / catch / throw can improve software
quality?
By eliminating one of the reasons for if statements.
The commonly used alternative to try / catch / throw is to return a return code (sometimes
called an error code) that the caller explicitly tests via some conditional statement such as if.
For example, printf(), scanf() and malloc() work this way: the caller is supposed to test
the return value to see if the function succeeded.
Although the return code technique is sometimes the most appropriate error handling
technique, there are some nasty side effects to adding unnecessary if statements:
Degrade quality: It is well known that conditional statements are approximately ten times more
likely to contain errors than any other kind of statement. So all other things being equal, if you can
eliminate conditionals / conditional statements from your code, you will likely have more robust
code.
Slow down time-to-market: Since conditional statements are branch points which are related to the
number of test cases that are needed for white-box testing, unnecessary conditional statements
increase the amount of time that needs to be devoted to testing. Basically if you dont exercise every
branch point, there will be instructions in your code that will never have been executed under test
conditions until they are seen by your users/customers. Thats bad.
Increase development cost: Bug finding, bug fixing, and testing are all increased by unnecessary
control flow complexity.
So compared to error reporting via return-codes and if, using try / catch / throw is likely to
result in code that has fewer bugs, is less expensive to develop, and has faster time-to-market.
Of course if your organization doesnt have any experiential knowledge of try / catch / throw,
you might want to use it on a toy project first just to make sure you know what youre doing
you should always get used to a weapon on the firing range before you bring it to the front lines
of a shooting war.
Im still not convinced: a 4-line code snippet shows that returncodes arent any worse than exceptions; why should I therefore
use exceptions on an application that is orders of magnitude
larger?
Because exceptions scale better than return-codes.
The problem with a 4-line example is that it has only 4 lines. Give it 4,000 lines and youll see
the difference.
Heres a classic 4-line example, first with exceptions:
1.
2.
3.
4.
5.
6.
try {
f();
// ...
} catch (std::exception& e) {
// ...code that handles the error...
}
Heres the same example, this time using return-codes (rc stands for return code):
1.
2.
3.
4.
5.
6.
int rc = f();
if (rc == 0) {
// ...
} else {
// ...code that handles the error...
}
People point to those toy examples and say, Exceptions dont improve coding or testing or
maintenance cost in that; why should I therefore use them in a real project?
Reason: exceptions help you with real-world applications. You wont likely see much if any
benefit on a toy example.
In the real world, the code that detects a problem must typically propagate error information
back to a different function that will handle the problem. This error propagation often needs
to go through dozens of functions f1() calls f2() calls f3(), etc., and a problem is
discovered way down in f10() (or f100()). The information about the problem needs to get
propagated all the way back to f1(), because only f1() has enough context to actually know
what should be done about the problem. In an interactive app, f1() is typically up near the
main event loop, but no matter what, the code that detects the problem often isnt the same as
the code that handles the problem, and the error information needs to get propagated through
all the stack frames in between.
Exceptions make it easy to do this error propagation:
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
void f1()
{
try {
// ...
f2();
// ...
} catch (some_exception& e) {
// ...code that handles the error...
}
}
void f2() { ...; f3(); ...; }
void f3() { ...; f4(); ...; }
void f4() { ...; f5(); ...; }
void f5() { ...; f6(); ...; }
void f6() { ...; f7(); ...; }
void f7() { ...; f8(); ...; }
void f8() { ...; f9(); ...; }
void f9() { ...; f10(); ...; }
void f10()
{
// ...
if ( /*...some error condition...*/ )
throw some_exception();
// ...
}
Only the code that detects the error, f10(), and the code that handles the error, f1(), have
any clutter.
However using return-codes forces error propagation clutter into all the functions in
between those two. Here is the equivalent code that uses return codes:
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
int f1()
{
// ...
int rc = f2();
if (rc == 0) {
// ...
} else {
// ...code that handles the error...
}
}
int f2()
{
// ...
int rc = f3();
if (rc != 0)
return rc;
// ...
return 0;
}
int f3()
{
// ...
int rc = f4();
if (rc != 0)
return rc;
// ...
return 0;
}
int f4()
{
// ...
int rc = f5();
if (rc != 0)
return rc;
// ...
return 0;
}
int f5()
{
// ...
int rc = f6();
if (rc != 0)
return rc;
// ...
return 0;
}
int f6()
{
// ...
int rc = f7();
if (rc != 0)
return rc;
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
// ...
return 0;
}
int f7()
{
// ...
int rc = f8();
if (rc != 0)
return rc;
// ...
return 0;
}
int f8()
{
// ...
int rc = f9();
if (rc != 0)
return rc;
// ...
return 0;
}
int f9()
{
// ...
int rc = f10();
if (rc != 0)
return rc;
// ...
return 0;
}
int f10()
{
// ...
if (...some error condition...)
return some_nonzero_error_code;
// ...
return 0;
}
The return-code solution spreads out the error logic. Functions f2() through f9() have
explicit, hand-written code related to propagating the error condition back up to f1(). That is
badness:
It clutters functions f2() through f9() with extra decision logic the most common cause of
bugs.
It clouds the simplicity of the programming logic in functions f2() through f9().
It requires the return value to perform two distinct duties functions f2() through f10() will need
to handle both my function succeeded and the result is xxx and my function failed and the error
information is yyy. When the types of xxx and yyy differ, you sometimes need extra by-reference
parameters to propagate both the successful and unsuccessful cases to the caller.
If you look very narrowly at f1() and f10() in the above examples, exceptions wont give you
much of an improvement. But if you instead open your eyes to the big picture, you will see a
substantial difference in all the functions in between.
Conclusion: one of the benefits of exception handling is a cleaner, simpler way to propagate
error information back to the caller that can handle the error. Another benefit is your function
doesnt needextra machinery to propagate both the successful and unsuccessful cases back to the
caller. Toy examples often dont emphasize either error propagation or handling of the tworeturn-types problem, therefore they dont represent Real World code.
Lets work a simple example: we would like to create a Number class that supports the four
arithmetic operations: add, subtract, multiply and divide. This is an obvious place for
overloaded operators, so lets define them:
1.
2.
3.
4.
5.
6.
7.
8.
class Number {
public:
friend Number operator+ (const Number& x, const Number& y);
friend Number operator- (const Number& x, const Number& y);
friend Number operator* (const Number& x, const Number& y);
friend Number operator/ (const Number& x, const Number& y);
// ...
};
3.
4.
5.
6.
7.
8.
9.
// ...
Number sum = x + y;
Number diff = x - y;
Number prod = x * y;
Number quot = x / y;
// ...
}
But then we have a problem: handling errors. Adding numbers could cause overflow, dividing
could cause divide-by-zero or underflow, etc. Whoops. How can we report both the I
succeeded and the result is xxx as well as I failed and the error information is yyy?
If we use exceptions, its easy. Think of exceptions as a separate return type that gets used only
when needed. So we just define all the exceptions and throw them when needed:
1. void f(Number x, Number y)
2. {
3.
try {
4.
// ...
5.
Number sum = x + y;
6.
Number diff = x - y;
7.
Number prod = x * y;
8.
Number quot = x / y;
9.
// ...
10. }
11. catch (Number::Overflow& exception) {
12.
// ...code that handles overflow...
13. }
14. catch (Number::Underflow& exception) {
15.
// ...code that handles underflow...
16. }
17. catch (Number::DivideByZero& exception) {
18.
// ...code that handles divide-by-zero...
19. }
20. }
But if we use return codes instead of exceptions, life gets hard and messy. When you cant
shove both the good number and the error information (including details about what went
wrong) inside the Number object, you will probably end up using extra by-reference parameters
to handle one of the two cases: either I succeeded or I failed or both. Without loss of
generality, I will handle the computed result via a normal return value and the I failed case
via a by-reference parameter, but you can just as easily do the opposite. Heres the result:
1. class Number {
2. public:
3.
enum ReturnCode {
4.
Success,
5.
Overflow,
6.
Underflow,
7.
DivideByZero
8.
};
9.
10. Number add(const Number& y, ReturnCode& rc) const;
11. Number sub(const Number& y, ReturnCode& rc) const;
12. Number mul(const Number& y, ReturnCode& rc) const;
The point of this is that you normally have to muck up the interface of functions that use
return codes, particularly if there is more error information to propagate back to the caller. For
example, if there are 5 error conditions and the error information requires different data
structures, you might end up with a fairly messy function interface.
None of this clutter happens with exceptions. Exceptions can be thought of as a separate return
value, as if the function automatically grows new return types and return values based on
what the function can throw.
Note: Please dont write me saying that you propose using return codes and storing the error
information in a namespace-scope, global or static variable, such as Number::lastError().
That isnt thread-safe. Even if you dont have multiple threads today, you rarely want to
permanently prevent anyone in the future from using your class with multiple threads.
Certainly if you do, you should write lots and lots of BIG UGLY COMMENTS warning future
programmers that your code is not thread-safe, and that it probably cant be made thread-safe
without a substantial rewrite.
What does it mean that exceptions separate the good path (or
happy path) from the bad path?
Its another benefit of exceptions over return-codes.
The good path (sometimes called the happy path) is the control-flow path that happens
when everything goes well when there are no problems.
The bad path (or error path) is the path that control-flow takes when something goes
wrong when there is a problem.
Exceptions, when done right, separate the happy path from the error path.
Here is a simple example: function f() is suppoesd to call functions g(), h(), i() and j(), in
sequence, as shown below. If any of those fail with a foo or bar error, f() is to handle the
error immediately then return successfully. If any other error occurs, f() is to propagate the
error information back to the caller.
Here is the code if exceptions are used:
The good path and the bad path are cleanly separated. The good (or happy) path is the
body of the try block you can read that linearly, and if there are no errors, control flows in a
simplistic path through those lines. The bad path is the body of the catch block and the body
of any matching catch blocks in any caller.
Using return codes instead of exception clutters this to the point where it is difficult to see the
relatively simple algorithm. The good (happy) and bad paths are hopelessly intermixed:
1. int f() // Using return-codes
2. {
3.
int rc; // "rc" stands for "return code"
4.
5.
GResult gg = g(rc);
6.
if (rc == FooError) {
7.
// ...code that handles "foo" errors...
8.
} else if (rc == BarError) {
9.
// ...code that handles "bar" errors...
10. } else if (rc != Success) {
11.
return rc;
12. }
13.
14. HResult hh = h(rc);
15. if (rc == FooError) {
16.
// ...code that handles "foo" errors...
17. } else if (rc == BarError) {
18.
// ...code that handles "bar" errors...
19. } else if (rc != Success) {
20.
return rc;
21. }
22.
23. IResult ii = i(rc);
24. if (rc == FooError) {
25.
// ...code that handles "foo" errors...
26. } else if (rc == BarError) {
27.
// ...code that handles "bar" errors...
28. } else if (rc != Success) {
29.
return rc;
30. }
31.
32. JResult jj = j(rc);
33. if (rc == FooError) {
34.
// ...code that handles "foo" errors...
35. } else if (rc == BarError) {
36.
// ...code that handles "bar" errors...
37. } else if (rc != Success) {
38.
return rc;
39. }
40.
41. // ...
42.
43. return Success;
44. }
By intermixing the good/happy path with the bad/error path, its harder to see what the code is
supposed to do. Contrast that with the version that used exceptions, which is almost selfdocumenting the basic functionality is very obvious.
Exception handling is not a free lunch. It requires discipline and rigor. To understand those
disciplines, you really should read the rest of the FAQ and/or one of the excellent books on the
subject.
Exception handling is not a panacea. If you work with a team that is sloppy and undisciplined,
your team will likely have problems no matter whether they use exceptions or return codes.
Incompetent carpenters do bad work even if they use a good hammer.
Exception handling is not one-size-fits-all. Even when you have decided to use exceptions rather
than return codes, that doesnt mean you use them for everything. This is part of the discipline: you
need to know when a condition should be reported via return-code and when it should be reported via
an exception.
Exception handling is a convenient whipping boy. If you work with people who blame their tools,
beware of suggesting exceptions (or anything else that is new, for that matter). People whose ego is
so fragile that they need to blame someone or something else for their screw-ups will invariably
blame whatever new technology was used. Of course, ideally you will work with people who are
emotionally capable of learning and growing: with them, you can make all sorts of suggestions,
because those sorts of people will find a way to make it work, and youll have fun in the process.
Fortunately there is plenty of wisdom and insight on the proper use of exceptions. Exception
handling is not new. The industry as a whole has seen many millions of lines of code and many
person-centuries of effort using exceptions. The jury has returned its verdict: exceptions can be
used properly, and when they are used properly, they improve code.
Learn how.
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 its 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 conditions
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 didnt happen because it didnt 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 Im 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 nullptr. However you discover that somebody
somewhere is sometimes passing nullptr anyway. There are two possibilities: either they are passing
nullptr 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 nullptr) 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 cant
detect by a careful code-review; it is not a bug). In the latter case, you should definitely fix the bug in
the callers 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: dont take those as hard and fast rules. They are guidelines, and there are
exceptions to each.
14.
// ...
15. }
16.
17. try {
18.
baz();
19. }
20. catch (BazException& e) {
21.
// ...
22. }
23. }
Although this uses the try/catch/throw syntax, the overall structure is very similar to the way
things are done with return codes, and the consequent software
development/test/maintenance costs are basically the same as they were for return codes. In
other words, this approach doesnt buy you much over using return codes. In general, it is bad
form.
One way out is to ask yourself this question for each try block: Why am I using a try block
here? There are several possible answers:
Your answer might be, So I can actually handle the exception. My catch clause deals with the error
and continues execution without throwing any additional exceptions. My caller never knows that the
exception occurred. My catch clause does not throw any exceptions and it does not return any errorcodes. In that case, you leave the try block as-is it is probably good.
Your answer might be, So I can have a catch clause that does blah blah blah, after which I will
rethrow the exception. In this case, consider changing the try block into an object whose destructor
does blah blah blah. For instance, if you have a try block whose catch clause closes a file then
rethrows the exception, consider replacing the whole thing with a File object whose destructor
closes the file. This is commonly called RAII.
Your answer might be, So I can repackage the exception: I catch a XyzException, extract the
details, then throw a PqrException. When that happens, consider a better hierarchy of exception
objects that doesnt require this catch/repackage/rethrow idea. This often involves broadening the
meaning of XyzException, though obviously you shouldnt go too far.
There are other answers as well, but the above are some common ones that Ive seen.
Main point is to ask Why?. If you discover the reason youre doing it, you might find that
there are better ways to achieve your goal.
Having said all this, there are, unfortunately, some people who have the return-code-mindset
burned so deeply into their psyche that they just cant seem to see any alternatives. If that is
you, there is still hope: get a mentor. If you see it done right, youll probably get it. Style is
sometimes caught, not just taught.
For constructors, yes: You should throw an exception from a constructor whenever you cannot
properly initialize (construct) an object. There is no really satisfactory alternative to exiting a
constructor by a throw. For more details, see here.
For destructors, not really: You can throw an exception in a destructor, but that exception must not
leave the destructor; if a destructor exits by emitting an exception, all kinds of bad things are likely to
happen because the basic rules of the standard library and the language itself will be violated. Dont
do it. For more details, see here.
Note: if a constructor finishes by throwing an exception, the memory associated with the object
itself is cleaned up there is no memory leak. For example:
1.
2.
3.
4.
5.
void f()
{
X x;
// If X::X() throws, the memory for x itself will not leak
Y* p = new Y(); // If Y::Y() throws, the memory for *p itself will not leak
}
There is some fine print on this topic, so you need to keep reading. Specifically you need to
know how to prevent memory leaks if the constructor itself allocates memory, and you also need to
be aware of what happens if you use placement new rather than the ordinary new used in the
sample code above.
exception, it might do something else). So the whole solution is harder to write. So the easy
thing to do is always do something else. That is,never throw an exception from a destructor.
Of course the word never should be in quotes since there is always some situation somewhere
where the rule wont hold. But certainly at least 99% of the time this is a good rule of thumb.
#include <memory>
class Fred {
public:
typedef std::unique_ptr<Fred> Ptr;
// ...
};
That typedef simplifies the syntax of all the code that uses your objects: your users can
say Fred::Ptr instead of std::unique_ptr<Fred>:
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
#include "Fred.h"
void f(std::unique_ptr<Fred> p); // explicit but verbose
void f(Fred::Ptr
p); // simpler
void g()
{
std::unique_ptr<Fred> p1( new Fred() ); // explicit but verbose
Fred::Ptr
p2( new Fred() ); // simpler
// ...
}
Using char*s like this is tedious and error prone. Why not just use an object of
some string class? Your compiler probably supplies a string-like class, and its probably just
as fast and certainly its a lot simpler and safer than the char* code that you would have to
write yourself. For example, if youre using the std::string class from the standardization
committee, your code might look something like this:
1.
2.
3.
4.
5.
6.
7.
8.
9.
#include <string>
The char* version requires you to write around three times more code than you would have to
write with the std::string version. Most of the savings came from std::strings automatic
memory management: in the std::string version, we didnt need to write any code
#include <stdexcept>
class MyException : public std::runtime_error {
public:
MyException() : std::runtime_error("MyException") { }
};
void f()
{
// ...
throw MyException();
}
Here, a temporary of type MyException is created and thrown. Class MyException inherits
from class std::runtime_error which (ultimately) inherits from class std::exception.
In fact, you have all the flexibility that you have in declaring function parameters, and the rules
for whether a particular exception matches (i.e., will be caught by) a particular catch clause are
almost exactly the same as the rules for parameter compatibility when calling a function.
Given all this flexibility, how do you decide what to catch? Simple: unless theres a good reason
not to, catch by reference. Avoid catching by value, since that causes a copy to be made and the
copy can have different behavior from what was thrown. Only under very special circumstances
should you catch by pointer.
11.
case 2: throw &y;
12.
}
13. }
14. catch (MyException* p) {
15.
// should we delete p here or not???!?
16. }
17. }
class MyException {
public:
// ...
void addInfo(const std::string& info);
// ...
};
void f()
{
10. try {
11.
// ...
12. }
13. catch (MyException& e) {
14.
e.addInfo("f() failed");
15.
throw;
16. }
17. }
In this example, the statement throw; means re-throw the current exception. Here, a
function caught an exception (by non-const reference), modified the exception (by adding
information to it), and then re-threw the exception. This idiom can be used to implement a
simple form of stack-trace, by adding appropriate catch clauses in the important functions of
your program.
Another re-throwing idiom is the exception dispatcher:
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
void handleException()
{
try {
throw;
}
catch (MyException& e) {
// ...code to handle MyException...
}
catch (YourException& e) {
// ...code to handle YourException...
}
}
void f()
{
try {
// ...something that might throw...
}
catch (...) {
handleException();
}
}
class MyExceptionBase { };
class MyExceptionDerived : public MyExceptionBase { };
void f(MyExceptionBase& e)
{
// ...
throw e;
}
10.
11. void g()
12. {
13. MyExceptionDerived e;
14. try {
15.
f(e);
16. }
17. catch (MyExceptionDerived& e) {
18.
// ...code to handle MyExceptionDerived...
19. }
20. catch (...) {
21.
// ...code to handle other exceptions...
22. }
23. }
If you try this, you might be surprised at run-time when your catch (...) clause is entered,
and not your catch (MyExceptionDerived&) clause. This happens because you didnt throw
polymorphically. In function f(), the statement throw e; throws an object with the same type
as the static type of the expression e. In other words, it throws an instance
of MyExceptionBase. The throw statement behaves as-if the thrown object is copied, as
opposed to making a virtual copy.
Fortunately its relatively easy to correct:
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
class MyExceptionBase {
public:
virtual void raise();
};
void MyExceptionBase::raise()
{ throw *this; }
class MyExceptionDerived : public MyExceptionBase {
public:
virtual void raise();
};
void MyExceptionDerived::raise()
{ throw *this; }
void f(MyExceptionBase& e)
{
// ...
e.raise();
}
void g()
{
MyExceptionDerived e;
try {
f(e);
}
catch (MyExceptionDerived& e) {
// ...code to handle MyExceptionDerived...
}
catch (...) {
// ...code to handle other exceptions...
34. }
35. }
Note that the throw statement has been moved into a virtual function. The
statement e.raise() will exhibit polymorphic behavior, since raise() is
declared virtual and e was passed by reference. As before, the thrown object will be of
the static type of the argument in the throw statement, but
within MyExceptionDerived::raise(), that static type is MyExceptionDerived,
not MyExceptionBase.
// wrap a raw C file handle and put the resource acquisition and release
// in the C++ type's constructor and destructor, respectively
class File_handle {
FILE* p;
public:
File_handle(const char* n, const char* a)
{ p = fopen(n,a); if (p==0) throw Open_error(errno); }
File_handle(FILE* pp)
{ p = pp; if (p==0) throw Open_error(errno); }
~File_handle() { fclose(p); }
operator FILE*() { return p; } // if desired
// ...
};
// use File_handle: uses vastly outnumber the above code
void f(const char* fn)
{
File_handle f(fn,"rw"); // open fn for reading and writing
22.
23.
24.
In a system, in the worst case we need a resource handle class for each resource. However,
we dont have to have a finally clause for each acquisition of a resource. In realistic systems,
there are far more resource acquisitions than kinds of resources, so the resource acquisition is
initialization technique leads to less code than use of a finally construct.
Also, have a look at the examples of resource management in Appendix E of TC++PL3e.