Chapter 19 - Exception
Handling
Chapter Goals
- To understand how exceptional conditions can arise within a program
- To understand how dealing with exceptional conditions can make your
programs more reliable and robust
- To learn how to use a variety of mechanisms to handle exceptional
conditions
- To understand how to use try blocks and catch handlers in your own
programs
19.1 Handling Exceptional
Situations
- Things sometimes go wrong. E.g.,
- User input errors
- Device errors
- Physical limitations
- Component (sw) failures
- Importance increases as complexity of systems increases
19.1 Handling Exceptional
Situations (cont.)
- Some different approaches we've seen to handling exceptional conditions:
- The fail predicate used by the stream I/O
library
- The assert macro
- Some errors can be detected and resolved at the point they occur
- Others are non-local; i.e., must be resolved at a higher-level
19.1 Handling Exceptional
Situations - Example
Consider a container class (chptr. 16), and programmers P1 and P2:
- P1 develops a stack that P2 will use
- P2 is ignorant of the implementation details, needs only the
interface:
Stack s;
s.push(3.14);
s.push(1.41);
s.push(2.76);
while (s.size() > 0)
{
cout << s.top() << "\n";
s.pop();
}
19.1 Handling Exceptional
Situations - Example
- Imagine that pop is called on an empty Stack
- P2 didn't understand the i/f, or
- P2 wrote a function that had a logic error
- P1 is unaware of the application P2 is creating
- P2 is in a better position to recover from the error
- Stack must report the error to the application written by
P2
19.2 Alternative Mechanisms for Handling
Exceptions
Alternative (historical) ways of handling exceptional conditions:
- Assume errors will not occur
- Print an error message
- Special return values
- External flags
- Use assert to halt execution
- Error Handlers (callback functions
19.3 Exceptions
try-catch-throw mechanism
- Later addition to C++
- An error is signaled by throwing an exception
- Any type can be thrown
- If not handled (caught) locally, function exits
- Does not return to caller
- Unwinds call stack, looking for an appropriate handler
19.3 Exceptions
Syntax 19.1:
Throwing an Exception
throw expression;
Example: |
throw logic_error("illegal future_value parameter");
|
Purpose: |
Abandon this function and throw a value
to an exception handler. |
|
19.3 Exceptions - Example
double future_value(double initial_balance, double p, int n)
{
if (p < 0 || n < 0)
{
logic_error description("illegal future_value parameter");
throw description;
}
return initial_balance * pow(1 + p / 100, n);
}
Note: logic_error is a
standard exception class, declared in <stdexcept>
19.3.1 Catching Exceptions
- Supply a handler with the try statement:
try
{
code
}
catch (logic_error& e)
{
handler
}
- If an error is thrown in the try clause, execution goes to
the catch clause
- If no appropriate handler is found, the next outer try block
is examined
19.3.1 Catching Exceptions - Syntax
Syntax 19.2 :
try Block
try
{
statements
}
catch (type_name1 variable_name1)
{
statements
}
catch (type_name2 variable_name2)
{
statements
}
...
catch (type_namen variable_namen)
{
statements
}
|
19.3.1 Catching Exceptions - Syntax (cont.)
Syntax 19.2 (cont.)
Example: |
try
{
List staff = read_list();
process_list(staff);
}
catch (logic_error& e)
{
cout << "Processing error " << e.what() << "\n";
}
|
Purpose: |
Provide one or more handlers for types of
exceptions that may be thrown when executing a block of statements.
|
|
19.3.1 try Block -
Example
- Place a handler into the main function that:
- tells the user that something has gone wrong, and
- offers a chance to try again with different inputs:
- Handler inspects the object thrown
- catch clause resembles a function, w/out a return
- catch calls the what member of logic_error
- what returns the string passed to e's c'tor
19.3.1 try Block -
Example
int main()
{
bool more = true;
while (more)
{
try
{
code
}
catch (logic_error& e)
{
cout << "A logic error has occurred: "
<< e.what() << "\n" << "Retry? (y/n)";
string input;
getline(cin, input);
if (input == "n") more = false;
}
}
}
19.3.2 Values Thrown and Caught
- Can throw (and catch) any type, including integer
- User-defined objects are commonly thrown
- Implicit conversion (e.g., int to double)
not performed on thrown values
19.3.2 Values Thrown and Caught
(cont.)
This doesn't work as intended:
try
{
. . .
throw "Stack Underflow";
. . .
}
catch (string err)
{
cerr << err << "\n";
}
19.3.2 (cont.) Throwing Objects
Users often define their own exceptions:
class MyApplicationError
{
public:
MyApplicationError(const string& r);
string& what() const;
private:
string reason;
};
MyApplicationError::MyApplicationError(const string& r)
: reason(r) {}
string& what() const
{
return reason;
}
19.3.2 (cont.) Throwing Objects
Errors are now indicated by throwing an instance of this class:
try
{
. . .
throw MyApplicationError("illegal value");
. . .
}
catch (MyApplicationError& e)
{
cerr << "Caught exception " << e.what() << "\n";
}
19.3.2 (cont.) Objects
as Exceptions
- We usually catch references:
- Efficiency
- Avoid slicing objects of inherited classes
- Use inheritance for exceptions:
- Reuse
- Standard interface
- Create categories, so handlers can be broad or specific
19.3.2 (cont.) Standard exception hierarchy
(partial), in <stdexcept>
19.3.2 (cont.) Inheriting From
Standard Exceptions
- Use library exceptions as-is (see initial example), or
- Inherit from these to create further specialized categories:
class FutureValueError : public logic_error
{
public:
FutureValueError(string reason);
};
FutureValueError::FutureValueError(string reason)
: logic_error(reason) {}
- No added attributes or functionality
- C'tor passes its argument to its parent class' c'tor
- All subclasses of exception have a what() method
19.3.2 (cont.) Exceptions & Inheritance
- Can still be caught with:
catch (logic_error& e)
- Or, can be caught with:
catch (FutureValueError& e)
19.3.2 Exceptions & Inheritance (cont.)
- Or, do both:
try
{
code
}
// only catches FutureValueError
catch (FutureValueError& e)
{ handler1 }
// catches all other logic_error
catch (logic_error& e)
{ handler2 }
catch (bad_alloc& e)
{ handler3 }
19.3.2 (cont.) catch Clauses
- Examined top to bottom
- Executes the first handler that matches, then stops processing
that exception
- Match derived class before its base class
- If uncaught, the previous try block is examined
- If call stack is unwound, std::terminate is called
- May specify your own by using std::set_terminate
19.3.2 (cont.) Example - catch
Clauses
Consider the function process_record, which calls read,
which in turn calls future_value, which throws an exception. Who
catches it?
void process_record()
{
try
{
read();
}
catch (logic_error& e)
{
cout << "caught logic_error " << e.what() << "\n";
}
}
19.3.2 (cont.) Example - catch
Clauses
void read()
{
try
{
. . .
double d = future_value();
}
catch (bad_alloc& e)
{
cout << "caught bad_alloc error " << e.what() << "\n";
}
}
double future_value(...)
{
. . .
throw FutureValueError("illegal future_value parameter");
}
19.3.2 (cont.) catch(...),
rethrow
- catch(...) used to catch any exception
- Should be last in list
- Exception unnamed
- Use throw w/no arguments to rethrow error
19.3.2 catch(...), rethrow (cont.)
try
{
code
}
catch (FutureValueError& e)
{
statements1
}
catch (...) // Catch any remaining exceptions
{
statements2
throw; // Rethrow the error
}
(See 19.3.3 for example)
Common Error 19.2
Throwing Objects versus Throwing Pointers
- Compare the following:
- throw FutureValueError("illegal parameter");
- throw new
FutureValueError("illegal parameter");
- b. must be caught like this:
catch (FutureValueError* e) ...
(or a ptr to a parent class)
- Who deletes the object?
19.3.3 Stack Unwinding
Example: reading input
- Product::read attempts to read a single Product from
input
- Returns false on the expected end of input
- Throws an exception if an error has occurred:
19.3.3 Stack Unwinding (cont.)
bool Product::read(fstream& fs)
{
getline(fs, name);
if (name == "") return false; // End of file
fs >> price >> score;
if (fs.fail())
throw runtime_error("Error while reading product");
string remainder;
getline(fs, remainder);
return true;
}
19.3.3 Stack Unwinding (cont.)
- C++ unwinds call stack in search of a try block to handle the
exception
- Before each function is terminated, destructors are called on all
stack variables
- First-class types don't have destructors:
Product* p = new Product();
if (p->read())
{
. . .
}
delete p; // Never executes if read throws an exception
19.3.3 (cont.) Exceptions and Cleaning
Up
- Make sure all stack values are instances of a class, or
- Catch the error, clean up, and rethrow the error:
Product* p = NULL;
try
{
p = new Product();
if (p->read())
{ . . . }
delete p;
}
catch (...)
{
delete p;
throw;
}
19.3.4 Exceptions and Constructors /
Destructors
- C'tors and d'tors do not return a value
- Throwing an exception is a clean way to indicate failure
- Caveat: Static values are initialized
before main is entered. There is no try block to catch
exceptions
- If a c'tor fails, the object is not created
- The d'tor isn't called
- Subtle source of leaks
(See next slide)
- Two concurrent exceptions will halt the program
- Don't throw exceptions from within a d'tor
19.3.4 (cont.) Memory Leak in a
Constructor
class DataArray
{
public:
DataArray(int s);
~DataArray();
void init(int s);
private:
int* data;
};
19.3.4 Memory Leak in a
Constructor (cont.)
DataArray::DataArray(int s)
{
data = new int[s];
init(s); // What happens if init throws exception?
}
DataArray::~DataArray()
{
delete[] data;
}
void DataArray::init(int s) throw (overflow_error)
{ . . . }
19.3.4 Memory Leak in a Constructor
(cont.)
Here's one solution:
DataArray::DataArray(int s)
{
data = new int[s];
try
{
init(s);
}
catch (...) // Catch any exception init throws
{
delete[] data;
data = NULL;
throw; // Rethrow exception
}
}
19.3.5 Exception Specifications
- Uncaught exceptions can be dangerous (Ariane rocket)
- Exceptions thrown by a function or method can be specified:
- An empty list denotes that no exceptions are thrown:
void process_products(fstream& fs) throw ()
- A function w/no throw specification can throw any exception
19.3.5 Exception Specifications (cont.)
Syntax 19.3 :
Exception Specification
return_type function_name(parameters)
throw (type_name1, type_name2, ..., type_namen)
}
Example: |
void process_products(fstream& fs)
throw (UnexpectedEndOfFile, bad_alloc)
|
Purpose: |
List the types of all exceptions that a function
can throw. |
|
19.3.5 Exception
Specifications (cont.)
Caveats:
- Compiler does not enforce them
- If a function indirectly throws an exception not in its specification,
the program is halted
- Specifications are not tied to function signatures during inheritance
- An override in a derived class can throw an exception not specified
in the base class definition
19.4 Case Study: Matrices
- Define 2 new classes:
- MatrixMismatchException, inherits from the standard class
invalid_argument
- MatrixIndexExceptions, inherits from out_of_range
- C'tor also takes the offending index value
- Uses a private method to append index to string before calling
the base-class c'tor
- The test program has an example of using a single catch clause to catch
both of these exceptions
19.4 Case Study: Matrices
(matrix3.h)
19.4 Case Study: Matrices
(matrix3.cpp)
19.4 Case Study: Matrices
(matrixtest3.cpp)
Chapter Summary
- Programs should be robust
- Select appropriate exception handling mechanism for the task
- Exception management becomes more important on multiple-developer
projects
- throw an exception to signal an error. One of the callers
must supply a try block w/an appropriate catch
clause