MODULE 4
Advanced Exception Handling: Derived
Class Exceptions and Handling Options
|
Learning
Objectives After completing this module,
students should be able to: •
Explain how handlers match exceptions of derived classes and why
the order of handlers matters. •
Choose between specific handlers, general handlers, catch (...), rethrowing,
and recovery. •
Explain the difference between throw; and throw e; (object
slicing). •
Write custom exception classes and exception hierarchies. •
Wrap one exception inside another with nested exceptions (C++11). |
1. Handling Derived Class
Exceptions
Exceptions can be objects of classes, and classes can be related by
inheritance. A handler whose parameter is a base class will also catch
objects of every class derived from it (publicly), because a derived object
is-a base object. This allows one handler to deal with a whole family of
related errors.
Figure 4.1: An
exception hierarchy: FileError, NetworkError and PaymentError derive from
std::runtime_error, which derives from std::exception.
1.1 Matching a derived exception
Program 4.1: A derived exception class and
two handlers: derived first, then base.
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
class FileError : public runtime_error
{
public:
FileError(const string &message) : runtime_error(message) {}
};
int main()
{
try {
throw
FileError("Unable to open the file");
}
catch
(const FileError &e) {
// most specific handler first
cout
<< "FileError caught: " << e.what() << endl;
}
catch
(const runtime_error &e) {
// base class handler
cout
<< "Runtime error: " << e.what() << endl;
}
return 0;
}
Output:
FileError caught: Unable to open the file
1.
FileError
derives from runtime_error, so a FileError object is also a runtime_error.
2.
The handlers are tried in order. catch (const FileError &) matches exactly and runs;
the base handler is skipped.
3.
If a different runtime_error
were thrown, the first handler would not match but the second one would.
1.2 Order of handlers: the most specific first
|
Important: Because the
first matching handler is used, handlers for derived classes must be
written before handlers for their base classes. If the base handler comes
first, it catches the derived objects as well, and the derived handler can
never run. |
Program 4.2: Wrong order – the base handler
is placed first.
#include <iostream>
#include <stdexcept>
using namespace std;
class FileError : public runtime_error
{
public:
FileError(const char *m) : runtime_error(m) {}
};
int main()
{
try {
throw
FileError("disk failure");
}
catch
(const runtime_error &e) {
// base handler placed FIRST
cout
<< "runtime_error handler: " << e.what() << endl;
}
catch
(const FileError &e) {
// unreachable
cout
<< "FileError handler: " << e.what() << endl;
}
return 0;
}
GCC detects the mistake and warns at compile time:
p4_order_bad.cpp:19:5: warning: exception of type
‘FileError’ will be caught by earlier handler [-Wexceptions]
p4_order_bad.cpp:16:5: note: for type
‘std::runtime_error’
Output:
runtime_error handler: disk failure
The FileError handler is unreachable:
the thrown object was received by the runtime_error
handler. The warning (-Wexceptions,
enabled by default) is a compiler diagnostic, not an error, so the program
still builds – always read warnings.
1.3 How a handler is selected

Figure 4.2: Handler
selection: the first matching handler runs; otherwise the search continues
outward.
Program 4.3: Four handlers, from the most
specific to the catch-all, and four different exceptions.
#include <iostream>
#include <stdexcept>
using namespace std;
class NetworkError : public runtime_error
{
public:
NetworkError() : runtime_error("Network connection failed") {}
};
void test(int k)
{
try {
if (k
== 1) throw NetworkError();
if (k
== 2) throw runtime_error("Generic runtime problem");
if (k
== 3) throw logic_error("Logic problem");
throw
5;
}
catch
(const NetworkError &e) {
cout
<< "Specific handler : " << e.what() << endl;
}
catch
(const runtime_error &e) {
cout
<< "Runtime handler : "
<< e.what() << endl;
}
catch
(const exception &e) {
cout
<< "General handler : "
<< e.what() << endl;
}
catch (...)
{
cout
<< "Catch-all handler: unknown type" << endl;
}
}
int main()
{
for (int k
= 1; k <= 4; k++)
test(k);
return 0;
}
Output:
Specific handler : Network connection failed
Runtime handler
: Generic runtime problem
General handler
: Logic problem
Catch-all handler: unknown type
|
Thrown
object |
Handler
that runs |
Reason |
|
NetworkError |
Specific handler |
Exact match |
|
runtime_error |
Runtime handler |
Exact match for the base class of NetworkError;
NetworkError handler does not match |
|
logic_error |
General handler (exception) |
logic_error derives from exception, not from runtime_error |
|
int (5) |
Catch-all |
No class handler can accept an int |
1.4 Catch by reference
A handler that takes a class object by value, catch (runtime_error e), makes a copy of only the
base-class part of a derived object. This is called object slicing: the
extra members and the overridden virtual functions of the derived object are
lost. Always catch class-type exceptions by reference to const: catch (const runtime_error &e). This also avoids an
unnecessary copy.
2. Exception Handling Options
When an exception has been caught, the handler may do one of several
things. The right choice depends on what the handler knows about the problem.
|
Option |
What
the handler does |
Typical
use |
|
Handle specifically |
Separate catch for each important type |
Different recovery actions for different errors |
|
Handle generally |
Catch a base class such as std::exception |
One handler for a family of errors; report what() |
|
Catch everything |
catch
(...) |
Last line of defence, logging, clean-up |
|
Rethrow |
throw; |
Do part of the work, let a caller finish |
|
Recover and retry |
Fix the cause and repeat the operation |
Invalid user input, temporary resource failure |
|
Translate or wrap |
Throw a different, more meaningful exception |
Hide low-level details from higher layers |
|
Terminate safely |
Log, release resources, exit |
Unrecoverable errors |
2.1 Specific handlers, general handlers and the
catch-all
Programs 4.1 and 4.3 showed the first three options. A good design places
specific handlers first, then a handler for the base class (usually const exception &), and, where necessary, catch (...) last. The catch-all handler cannot know the
type of the exception, so it can only report that something went wrong, clean
up and possibly rethrow.
2.2 Rethrowing an exception
A handler can pass the exception on to an outer handler with a throw; statement without an operand. The original
exception object, with its real type and data, is passed on.
Program 4.4: Log the error in the inner
function and let main() handle it.
#include <iostream>
#include <stdexcept>
using namespace std;
void process()
{
try {
throw
runtime_error("Database connection failed");
}
catch
(const exception &e) {
cout
<< "Logged in process(): " << e.what() << endl;
// pass
the same exception outward
throw;
}
}
int main()
{
try {
process();
}
catch
(const exception &e) {
cout
<< "Handled in main(): " << e.what() << endl;
}
return 0;
}
Output:
Logged in process(): Database connection failed
Handled in main(): Database connection failed
Figure 4.3: Function
B logs the exception and rethrows the same object to main().
throw; versus throw e;
Writing throw e; inside a handler is
different: it creates a new exception object copied from the variable e, whose declared type may be a base class. If e was declared as a reference to a base class, the copy
is sliced and the derived type is lost.
Program 4.5: The effect of throw; and throw e;
on a derived exception.
#include <iostream>
#include <stdexcept>
#include <typeinfo>
using namespace std;
class PaymentError : public runtime_error
{
public:
PaymentError(const char *m) : runtime_error(m) {}
};
void rethrowCorrectly()
{
try {
throw
PaymentError("Payment declined");
}
catch
(exception &e) {
throw; // the
original object is passed on
}
}
void rethrowWithCopy()
{
try {
throw
PaymentError("Payment declined");
}
catch
(exception &e) {
throw
e; // a NEW exception
object is copied from e
}
}
int main()
{
try {
rethrowCorrectly(); }
catch
(const PaymentError &e) {
cout
<< "throw; ->
PaymentError caught: " << e.what() << endl;
}
catch
(const exception &e) {
cout
<< "throw; -> only
exception caught" << endl;
}
try {
rethrowWithCopy(); }
catch
(const PaymentError &e) {
cout
<< "throw e; -> PaymentError caught: " << e.what()
<< endl;
}
catch
(const exception &e) {
cout
<< "throw e; -> only exception caught (object sliced)"
<< endl;
}
return 0;
}
Output:
throw; ->
PaymentError caught: Payment declined
throw e; -> only exception caught (object sliced)
|
Important: To pass an
exception on, always use throw;, never throw
e;. |
2.3 Recovering from an exception
Sometimes the program can correct the cause of the problem and try again.
A typical case is invalid input. The try-catch
must then be inside a loop, and the cause (for example, the failed state
of cin) must be cleared before the
next attempt.
Program 4.6: Keep asking for the age until
the input is valid.
#include <iostream>
#include <limits>
#include <stdexcept>
using namespace std;
int readAge()
{
int age;
cout
<< "Enter age: ";
if (!(cin
>> age)) {
//
reset the stream state
cin.clear();
//
discard the bad line
cin.ignore(numeric_limits<streamsize>::max(), '\n');
throw
runtime_error("Input is not a number");
}
if (age
< 0 || age > 120)
throw
invalid_argument("Age must be between 0 and 120");
return age;
}
int main()
{
int age =
-1;
bool valid
= false;
// keep
asking until the input is valid
while
(!valid) {
try {
age
= readAge();
valid = true;
}
catch
(const invalid_argument &e) {
cout << "Invalid value: " << e.what() <<
endl;
}
catch
(const runtime_error &e) {
cout << "Bad input: " << e.what() << endl;
}
}
cout
<< "Valid age: " << age << endl;
return 0;
}
Output for the inputs abc, -5 and 25:
Enter age: abc
Bad input: Input is not a number
Enter age: -5
Invalid value: Age must be between 0 and 120
Enter age: 25
Valid age: 25
•
For the text abc
the extraction from cin fails. cin.clear() resets the error state and cin.ignore(...) removes the bad line; the function then
throws runtime_error.
•
For -5 the
number is read but is outside the valid range, so invalid_argument
is thrown.
•
In both cases the loop continues and asks again; valid becomes true
only when readAge() returns without
throwing.
•
The handler for invalid_argument
is written before the one for runtime_error
because both derive from exception
but are unrelated to each other; the order here is a matter of clarity.
2.4 Translating and wrapping exceptions
A low-level error such as “disk read failed” is meaningless for the code
that only wanted to load a configuration. A handler can therefore throw a more
meaningful exception, keeping the original one inside it. The C++11
facilities std::throw_with_nested() and std::rethrow_if_nested() do this.
Program 4.7: Wrapping an exception and
printing the whole chain.
#include <iostream>
#include <exception>
#include <stdexcept>
#include <string>
using namespace std;
void lowLevel()
{
throw
runtime_error("disk read failed");
}
void highLevel()
{
try {
lowLevel();
}
catch (...)
{
// wrap
the original exception inside a more meaningful one
throw_with_nested(runtime_error("cannot load the
configuration"));
}
}
/* Print an exception and, recursively, the
exceptions nested inside it */
void printAll(const exception &e, int level = 0)
{
cout
<< string(level * 2, ' ') << e.what() << endl;
try {
rethrow_if_nested(e);
}
catch
(const exception &inner) {
printAll(inner, level + 1);
}
}
int main()
{
try {
highLevel();
}
catch
(const exception &e) {
printAll(e);
}
return 0;
}
Output:
cannot load the configuration
disk read
failed
|
Version Note: Nested
exceptions (throw_with_nested, rethrow_if_nested,
std::nested_exception)
and std::exception_ptr
(current_exception,
rethrow_exception)
were added in C++11. They are not available in C++98/03; Program 4.7
does not compile with -std=c++98. |
3. Custom Exception Classes and
Hierarchies
Throwing a plain int or
string tells the handler very little. A user-defined class can carry an error
message, an error code and any other useful data, and can be placed in a
hierarchy so that handlers can be as specific or as general as needed.
Guidelines.
•
Derive custom exceptions from std::exception
(or from runtime_error or logic_error) so that generic catch
(const exception &) handlers work for them.
•
Override what()
to return a description of the error.
•
Throw objects by value and catch them by
reference to const.
•
Make a base class for the errors of one library or
program, and derive specific errors from it.
Program 4.8: A hierarchy AppError →
DatabaseError and NetworkError with error codes.
#include <iostream>
#include <string>
using namespace std;
/* A base class for all errors of an application. It
derives from std::exception
so that
generic handlers still work. */
class AppError : public exception
{
string
message;
int code;
public:
AppError(const string &m, int c) : message(m), code(c) {}
const char
*what() const noexcept override { return message.c_str(); }
int
errorCode() const { return code; }
};
class DatabaseError : public AppError
{
public:
DatabaseError(const string &m) : AppError("Database: " +
m, 101) {}
};
class NetworkError : public AppError
{
public:
NetworkError(const string &m) : AppError("Network: " + m,
202) {}
};
void run(int k)
{
try {
if (k
== 1) throw DatabaseError("connection lost");
if (k
== 2) throw NetworkError("timeout");
throw
AppError("unknown failure", 999);
}
catch
(const DatabaseError &e) {
cout
<< "DB handler ->
" << e.what() << " (code " << e.errorCode()
<< ")" << endl;
}
catch
(const AppError &e) {
// any other application error
cout
<< "AppError handler -> " << e.what() << "
(code " << e.errorCode()
<< ")" << endl;
}
}
int main()
{
run(1);
run(2);
run(3);
return 0;
}
Output:
DB handler
-> Database: connection lost (code 101)
AppError handler -> Network: timeout (code 202)
AppError handler -> unknown failure (code 999)
•
AppError
overrides what() and adds an error code. DatabaseError and NetworkError
add a prefix to the message and fix their code.
•
In run(1)
the specific DatabaseError handler runs. For NetworkError and for a plain AppError
there is no specific handler, so the AppError
handler (base class) receives them. This is the benefit of the hierarchy: a new
kind of error needs no new handler unless it needs special treatment.
|
Version Note: Signature
of what().
In C++11 and later it is written const char *what() const noexcept
override. Before C++11 it was virtual
const char *what() const throw(), using a dynamic
exception specification. throw() as a
specification is deprecated since C++11 and removed in C++20 (Module 5). New
code should use noexcept; the keyword override
also requires C++11. |
4. Good Practices
|
Practice |
Reason |
|
Throw by value, catch by const
reference |
No slicing, no unnecessary copies, no pointer ownership
problems |
|
Derive from std::exception |
Generic handlers and what() work for your classes |
|
Order handlers from derived to base, then catch (...) |
The first match wins |
|
Use throw; to rethrow |
Preserves the original type |
|
Do not throw pointers to local objects |
The object no longer exists when the handler runs |
|
Do not let exceptions leave destructors |
A second exception during unwinding terminates the program
(Module 6) |
|
Catch only what you can handle |
Let other exceptions propagate to code that knows more |
5. Quick Revision
•
A base-class handler catches derived exceptions; derived
handlers must come first.
•
Catch class exceptions by const
reference; throw; keeps the original object,
throw e; may slice it.
•
Options: specific handler, general handler, catch (...), rethrow, recover (loop), wrap (nested
exceptions), terminate safely.
•
Custom exceptions derive from std::exception
and override what() (noexcept
in C++11 and later).
6. Important Examination
Questions
Short-answer questions
1.
How does a base class handler treat objects of derived
classes?
2.
Why must derived-class handlers be placed before
base-class handlers?
3.
What is object slicing? How is it avoided in exception
handling?
4.
Differentiate between throw;
and throw e;.
5.
Why should exception classes override what()?
6.
What is the purpose of nested exceptions?
Long-answer and
programming questions
1.
Explain handling of derived class exceptions with a
program and the correct order of handlers.
2.
Explain the options available for handling an
exception, with an example of each.
3.
Write a program that defines an exception hierarchy and
catches it with specific and general handlers.
4.
Write a program that recovers from invalid input using
exceptions and a loop.
5.
Explain rethrowing of exceptions with a program.
6.
Write a custom exception class with a message and an
error code.
No comments:
Post a Comment