Table of Contents
We all know that things sometimes break and that improbable or unforeseen events can mess up the most carefully laid plans. This is especially true with programming and might explain why programmers have such great respect for Murphy's Law. [1] When we write programs we try to anticipate the things that can go wrong and make allowances for them; i.e., we try to make our programs foolproof. This may seem inconsistent with a very important corollary to Murphy's law, [2] but we persevere anyway.
Exception handling permits the program to respond to an exceptional situation by invoking a procedure that is specifically designed to handle it. This can enable the program to recover from a condition that might normally cause it to crash or get into a bad state.
Exception handling was added to C++ relatively late in the development of the language. As with any good thing, there is a cost associated with exception handling. Enabling exception handling adds a significant amount of additional code to the executable, which can degrade runtime performance. Some developers avoid this feature completely. Qt library code, for example, does not require any exception handling in client code.
An exception is an object or piece of data that is thrown from one place (the location of the error) to another (a catch
statement, which contains the appropriate code for handling the situation).
The context for exception handling is a try
block which is followed by one or more catch
statements.
Here is an arrangement that might be used to handle exceptions.
try { // to do something } catch ( ExceptionType1 & refName ) { // handle one ExceptionType } ... catch ( ExceptionTypeN & optionalParam ) { // statements for processing one of the possible thrown exceptions. }
The keyword throw
can be followed by an expression of any type.
In particular, this includes
Standard exceptions
Built-in types
Custom classes
The exception handling mechanism can transmit information about a runtime problem.
Even though it is perfectly legal to throw
a piece of data of a simple type (e.g., int
), this is not regarded as good practice.
Instead, it is best to work with std::exception
objects of classes with descriptive names, and a consistent interface
that can be used by client code to increase the informational value of catch
statements.
Example 1 contains some custom exception types.
Example 1. src/exceptions/example/exceptions.h
[ . . . . ] /* a custom exception with no data */ class SizeMatchError { public: SizeMatchError() {} QString what() { return "Size mismatch for Vector addition!"; } }; /* a custom exception with data */ class BadSizeError { public: BadSizeError(int sz): m_Sz(sz) {} QString what() const { return QString("Invalid size for Vector: %1").arg(m_Sz); } int m_Sz; }; #include <stdexcept> using std::exception; /* a custom exception extending from std::exception */ class RangeError : public exception { public: RangeError() {} const char* what() const throw() {return "Subscript out of range!"; } }; [ . . . . ]
In Section 3 we discuss function signatures like that of RangeError::what()
.
Generally, throw
statements occur inside function definitions, and transmit information to the client code about an exceptional circumstance.
The expression inside the throw is copied into a temporary buffer, making it possible to throw temporary stack objects.
A throw
statement resembles a function call, but really it is more like an express-return
.
This is why:
A throw
statement always returns information to an earlier position in the program stack.
There is no way to go "back" to the location of the throw
, because throw
has gone "back" already.
The stack is unwound, meaning that all stack objects are popped and destroyed, until we reach the stack frame corresponding to a try/catch
block with a compatible catch
parameter.
If a matching catch
statement is found, its code is executed.
If no matching catch
is found, the default handler (terminate()
or abort()
) is called, which results in the program terminating.
throw()
in a Function Signature
The ANSI/ISO standard permits member function declarations in class definitions to specify which exceptions might be thrown when the function is called.
This declaration syntax informs the authors of client code so that they can place try
blocks and catch
statements appropriately.
A throw()
expression in a function declaration is part of that function's signature.
The template-based Vector
class shown in Example 2 throws a variety of different kinds of exceptions, to demonstrate this feature.
Example 2. src/exceptions/example/vector.h
[ . . . . ] #include "exceptions.h" using std::bad_alloc; #include <QString> #include <QStringList> #include <QTextStream> template <class T> class Vector { public: typedef T* iterator; explicit Vector(int n = 100) throw(BadSizeError, bad_alloc); Vector(const Vector & v) throw(bad_alloc); Vector(const T* a, int n) throw(BadSizeError, bad_alloc); ~Vector(); QString toString() const; iterator begin() const { return m_P; } iterator end() const { return m_P + m_Size; } T& operator[](int i) throw(RangeError); Vector& operator=(const Vector& v) throw(bad_alloc); Vector operator+(const Vector& v) const throw(SizeMatchError); private: int m_Size; T* m_P; void copy(const T* a, int n) throw(BadSizeError, bad_alloc); };
The conditions for each throw
are specified in the member function definitions, shown in Example 3.
Notice we have function definitions in a header file; this is because they are template functions.
Example 3. src/exceptions/example/vector.h
[ . . . . ] template <class T> Vector<T>:: Vector(int n) throw(BadSizeError, bad_alloc) : m_Size(n) { if(n <= 0) throw BadSizeError(n); m_P = new T[m_Size];} [ . . . . ] template <class T> T& Vector<T>:: operator[](int i) throw(RangeError) { if(i >= 0 && i < m_Size ) return (m_P[i]); else throw RangeError(); } [ . . . . ] template <class T> Vector<T> Vector<T>:: operator+(const Vector& v) const throw(SizeMatchError) { if(m_Size != v.m_Size) { throw SizeMatchError(); } else { Vector sum(m_Size); for(int i = 0; i < m_Size; ++i) sum.m_P[i] = m_P[i] + v.m_P[i]; return sum; } }
Exceptions are raised when certain kinds of operations fail during the execution of a function.
If an exception is thrown from within a try
block (perhaps deeply nested within the block), it can be handled by a catch
statement with a parameter that is compatible with the exception type.
The syntax of a try
block has the following form:
try compoundStatement handlerList
The order in which handlers are defined determines the order that they will be tested against the type of the thrown expression. It is an error to list handlers in an order that prevents any of them from being called.[3]
The throw
expression matches the catch
parameter type if it is assignment-compatible with that type.
The syntax of a handler has the following form:
catch (formalArgument) compoundStatement
A catch
statement looks like the definition of a function that has one parameter but no return type.
It is a good idea to declare the formalArgument as a reference, to avoid making an unnecessary copy.
If a thrown exception is not caught by an appropriate handler, the default handler will abort the program.
Example 4. src/exceptions/catch.cpp
#include <iostream> #include <cstdlib> using namespace std; void foo() { int i, j; i = 14; j = 15; throw i; } void call_foo() { int k; k = 12; foo(); throw ("This is from call_foo"); } void call_foo2() { double x = 1.3; unsigned m = 1234; throw (x); throw m; } int main() { try { call_foo(); call_foo2(); } catch(const char* message) {cerr << message << endl; exit(1); } catch(int &n) { cout << "caught int " << n << endl; } catch(double& d) {
cout << "caught a double:" << d << endl; } catch( ... ) {
cerr << "ellipsis catch" << endl; abort(); } } Output: # with the first throw commented out src/generic> g++ catch.cpp src/generic> ./a.out This is from call_foo src/generic> # with the first two throws commented out src/generic> g++ catch.cpp src/generic> ./a.out caught a double src/generic> # with the first three throws commented out src/generic> g++ catch.cpp src/generic> ./a.out ellipsis catch Aborted src/generic> # with all the throws enabled src/generic> g++ catch.cpp src/generic> ./a.out caught int 14 src/generic>
Example 4 demonstrates a few possibilities for handlers.
The formal parameter of catch()
can be abstract (i.e., it can have type information without a variable name).
The final catch(...)
can use an ellipsis and matches any exception type.
The system calls clean-up functions, including destructors for stack objects and for objects local to the try block.
When the handler has completed execution, if the program has not been terminated, execution will resume at the first statement following the try
block.
Example 5 shows come client code for the Vector
template class and exception classes that we defined in the preceding sections.
Depending on how much system memory you have on your computer, you may need to adjust the initial values of BIGSIZE
and WASTERS
to get this program to run properly.
The output is included after the code.
Example 5. src/exceptions/example/exceptions.cpp
#include "vector.h" #include <QTextStream> #include <QDebug> QTextStream cout(stdout); void g (int m) { static int counter(0); static const int BIGSIZE(50000000), WASTERS(6); ++counter; try { Vector<int> a(m), b(m), c(m); qDebug() << "in try block, doing vector calculations. m= " << m ; for (int i = 0; i < m; ++i) { a[i] = i; b[i] = 2 * i + 1; } c = a + b; qDebug() << c.toString(); if (counter == 2) int value = c[m];if (counter ==3) { Vector<int> d(2*m); for (int i = 0; i < 2*m; ++i) d[i] = i * 3; c = a + d;
} if (counter == 4) { for (int i = 0; i < WASTERS; ++i)
double* ptr = new double(BIGSIZE); Vector<int> d(100 * BIGSIZE); Vector<int> e(100 * BIGSIZE);
for (int i = 0; i < BIGSIZE; ++i) d[i] = 3 * i; } } catch(BadSizeError& bse) {
qDebug() << bse.what() ; } catch(RangeError& rer) { qDebug() << rer.what() ; } catch(SizeMatchError& sme) { qDebug() << sme.what(); } catch(...) { qDebug() << "Unhandled error! Aborting..."; abort(); } qDebug() << "This is what happens after the try block." ; } int main() { g(-5);
g(5); g(7); g(9); } Output: src/exceptions/example> ./example Invalid size for Vector: -5 This is what happens after the try block. in try block, doing vector calculations. m= 5 <1, 4, 7, 10, 13> Subscript out of range! This is what happens after the try block. in try block, doing vector calculations. m= 7 <1, 4, 7, 10, 13, 16, 19> Size mismatch for Vector addition! This is what happens after the try block. in try block, doing vector calculations. m= 9 <1, 4, 7, 10, 13, 16, 19, 22, 25> Unhandled error! Aborting... Aborted src/exceptions/example>
Because we did not have a handler for the bad_alloc
exception, the default handler was called.
Make some changes to Example 5 to better understand exceptions. Try the following experiments and write your observations.
What happens if we omit/remove the default handler?
What happens if we omit/remove one of the exception types from the throw()
list in the member function header?
Syntactically, throw
can be used in three ways:
throw
expression This form raises an exception. The innermost try
block in which an exception is raised is used to select the catch
statement that handles the exception.
throw
This form, with no argument, is used inside a catch
to rethrow the current exception. It is typically used when you want to propagate the exception to the next outer level.
throw
(exceptionType list); An exception specification is part of the function signature. A throw
following a function prototype indicates that the exception could be thrown from inside the function body and, therefore, should be handled by client code.
This construct is somewhat controversial. The C++ developer community is split about when it is a good idea to use exception specifications.
In Example 6 we make use of the fact that C++ allows composite objects to be thrown. We define a class specifically so that we can throw an object of that type if necessary. We use the quadratic formula to compute one of the roots of a quadratic equation and we must be careful not to allow a negative value to be passed to the square root function.
Example 6. src/exceptions/throw0/throw0.cpp
[ . . . . ] class NegArg { public: NegArg(double d) : m_Val(d), m_Msg("Negative value") {} QString getMsg() { return QString("%1: %2").arg(m_Msg).arg(m_Val); } private: double m_Val; QString m_Msg; }; double discr(double a, double b, double c) { double d = b * b - 4 * a * c; if (d < 0) throw NegArg(d); return d; } double quadratic_root1(double a, double b, double c) { return (-b + sqrt(discr(a,b,c))/(2 * a)); } int main() { try { qDebug() << quadratic_root1(1, 3, 1) ; qDebug() << quadratic_root1(1, 1, 1) ; } catch(NegArg& narg) { qDebug() << "Attempt to take square root of " << narg.getMsg() << endl; } qDebug() << "Just below the try block." ; } Output: -1.88197 Attempt to take square root of Negative value: -3 Just below the try block.
The NegArg
object thrown by the discr()
function persists until the handler with the appropriate signature, catch(NegArg)
exits.
The NegArg object is available for use inside the handler - in this case to display some information about the problem.
In this example, the throw prevented the program from attempting to compute the square root of a negative number.
When a nested function throws an exception, the process stack is "unwound" until an exception handler with the right signature is found.
Using throw
without an expression rethrows a caught exception.
The catch
that rethrows the exception presumably cannot complete the handling of the existing exception, so it passes control to the nearest surrounding try
block, where a suitable handler with the same signature (or ellipsis) is invoked.
The exception expression continues to exist until all handling is completed.
If the exception handler does not terminate the execution of the program, execution resumes below the outermost try
block that last handled the rethrown expression.
Example 7 attempts to illustrate how an outer try
can handle exceptions that the inner one delegates.
Example 7. src/exceptions/throw2/throw2.cpp
#include <iostream> void foo() { int i, j; i = 14; j = 15; throw i; } void call_foo() { int k; k = 12; foo(); } int main() { using namespace std; try { cout << "In the outer try block" << endl; try { call_foo();} catch(int n) { cout << "I can't handle this exception!" << endl; throw; } } catch(float z) { cout << "Wrong catch!" << endl; } catch(char s) { cout << "This is also wrong!" << endl; } catch(int n) { cout << "\ncaught it " << n << endl; } cout << "Just below the outermost try block." << endl; } Output: In the outer try block I can't handle this exception! caught it 14 Just below the outermost try block.
![]() | Note |
---|---|
Remember that we do not recommend throwing basic types such as |
As we just saw, it is often a good idea to package exception information as an object of a class.
The thrown expression can then provide information that the handler can use when it executes.
For example, such a class could have several constructors. The throw
can supply appropriate arguments for the particular constructor that fits the exception.
class VectError { private: int m_Ub, m_Index, m_Size; public: VectError(Error er, int ix, int ub); // subscript out of bounds VectError(Error er, int sz); // out of memory enum Error { BOUNDS, HEAP, OTHER } m_ErrType; [ . . . ] };
With such a class definition, an exception can be thrown as follows:
throw VectError(VectError::BOUNDS, i, ub); or throw VectError(VectError::HEAP, size);
![]() | Question |
---|---|
Notice that this is a temporary object that is being thrown, but the |
Exception objects are copied into a special location (not the stack) before the stack is unwound.
It is possible to nest try
blocks.
If no matching handler is available in the immediate try
block, the search continues, stepwise, in each of the surrounding try
blocks.
If no handler can be found that matches, then the default handler is used; i.e., terminate()
.
Every book on C++ has a section on handling new failures. The way to handle such failures tends to vary, because the behavior of a C++ program when it runs out of memory is not the same from one platform to another.
We begin our discussion with a caveat. When a C++ program has a memory leak and runs for a long time, eventually there will be no memory available to it. You might think that would cause an exception to be thrown. However most desktop operating systems (including *nix and Win32) implement virtual memory, which permits the operating system, when its random access memory (RAM) fills up beyond some preset level, to copy the contents of memory that has not been used recently to a special place on the system disk drive. This substitution of relatively slow memory (disk storage) for fast memory (RAM) is generally invisible to the user (except for the performance degradation). Hand-held devices are another matter, and may have more strict memory restrictions imposed, rather than permitting any virtual memory swapping at all. If the demands on the system RAM are especially heavy, an OS may use virtual memory to keep satisfying allocation requests until the system starts thrashing.[4] When this happens, the whole system grinds to a halt until the system administrator can intervene and kill the memory-eating process. At no point will any of the memory allocation failure-handling code be reached in the errant process. It is for these reasons that memory allocation errors are handled differently, or not at all, depending on the designated platform.
Having said this, the ANSI/ISO standard does specify that the free store operator new
should throw a bad_alloc
exception instead of returning NULL
if it cannot carry out an allocation request.
If a thrown bad_alloc
exception is not caught by a catch()
block, the "default exception handler" is called, which could be either abort()
or terminate()
.
Example 8 demonstrates this feature of C++.
Example 8. src/newfailure/bad-alloc1.cpp
#include <iostream>
#include <new>
using namespace std;
void memoryEater() {
int i = 0;
double* ptr;
try {
while (1) {
ptr = new double[50000000];
cerr << ++i << '\t' ;
}
} catch (bad_alloc& excpt) {
cerr << "\nException occurred: "
<< excpt.what() << endl;
}
}
int main() {
memoryEater();
cout << "Done!" << endl;
return 0;
}
Output:
src/newfailure> g++ bad-alloc1.cpp
src/newfailure> ./a.out
1 2 3 4 5 6 7
Exception occurred: St9bad_alloc
Done!
src/newfailure>
![]() | Question |
---|---|
Why were we able to reach the exception handling code in this example, without causing any thrashing? |
We can specify what new
should do when there is not enough memory to satisfy an allocation request.
When new
fails, it first calls the function specified by set_new_handler().
If new_handler
has not been set, a bad_alloc
object is thrown that can be queried for more information by calling one of its member functions, as shown in Example 8.
Example 9 shows how to specify our own new_handler
.
Example 9. src/newfailure/setnewhandler.cpp
#include <iostream>
#include <cstdlib>
#include <new>
using namespace std;
void memoryEater() {
int i = 0;
double* ptr;
while (1) {
ptr = new double[50000000];
cerr << ++i << '\t' ;
}
}
void out_of_store() {
cerr << "\noperator new failed: out of store\n";
exit(1);
}
int main() {
set_new_handler(out_of_store);
memoryEater();
cout << "Done!" << endl;
return 0;
}
Output:
src/newfailure> g++ setnewhandler.cpp
src/newfailure> ./a.out
1 2 3 4 5 6 7
operator new failed: out of store
OOP>
Note the absence of a try
block.
![]() | Question |
---|---|
What happens if the last command in the |
Example 10 combines both the new_handler
and an exception, by throwing a standard exception from the new_handler
.
In this way, you can intercept the default behavior of a bad_alloc
, but perform some custom operations before conditionally throwing the normal exception.
Example 10. src/newfailure/bad-alloc2.cpp
#include <iostream>
#include <cstdlib>
#include <new>
using namespace std;
void memoryEater() {
int i = 0;
double* ptr;
try {
while (1) {
ptr = new double[50000000];
cerr << ++i << '\t' ;
}
} catch(bad_alloc& excpt) {
cerr << "\nException occurred: "
<< excpt.what() << endl;
}
}
void out_of_store() {
cerr << "\noperator new failed: out of store\n";
throw bad_alloc();
}
int main() {
set_new_handler(out_of_store);
memoryEater();
cout << "Done!" << endl;
return 0;
}
Output:
src/newfailure> g++ bad-alloc2.cpp
src/newfailure> ./a.out
1 2 3 4 5 6 7
operator new failed: out of store
Exception occurred: St9bad_alloc
Done!
src/newfailure>
You may encounter the old null-checking style for detecting failures of new
in legacy code.
That's a sure sign that there are going to be problems with maintenance.
Fortunately, there is a simple way to update that old approach.
In Example 11, we add the qualifier (nothrow)
to the allocation statement.
As its name suggests, this qualifier supresses the throwing of bad_alloc
and enables new
to return a 0
pointer if it fails.
Example 11. src/newfailure/nullchecking.cpp
#include <iostream>
#include <new>
using namespace std;
void memoryEater() {
int i = 0;
double* ptr;
while (1) {
ptr = new (nothrow) double[50000000];
if (ptr == 0)
return;
cerr << ++i << '\t' ;
}
}
int main() {
memoryEater();
cout << "Done!" << endl;
return 0;
}
Output:
src/newfailure> g++ nullchecking.cpp
src/newfailure> ./a.out
1 2 3 4 5 6 7 Done!
src/newfailure>
According to [Sutter2001], nothrow
is to be avoided, because it offers no clear advantages over throwing/handling exceptions, and an uncaught memory exception does perform some cleanup, while an uncaught null pointer exception usually terminates without any kind of cleanup. However, on systems with virtual memory, there is little point in checking for new
failures anyway, because an errant program will often bring the virtual memory system into a thrashing state before any of the new failure handling code is reached.
[4] When a system is constantly swapping memory back and forth to disk, preventing other I/O from happening, we call that "thrashing."
It is often a good idea to organize exception types in hierarchies. This is done for the same reasons that we organize any classes in hierarchies.
Assume that DerivedTypeError
is derived from BaseTypeError
. How many errors can you find in the following sequence of handlers.
catch(void*) // any char* would match catch(char*) catch(BaseTypeError&) // any DerivedTypeError& would match catch(DerivedTypeError&)
As we have seen, you can throw
standard exceptions, custom classes, or even basic types (but please don't throw those). The following examples demonstrate the use of custom exceptions inside the scope of a namespace
.
There are function defintions missing from Example 14, which need to be defined before this application is built. Define the missing functions.
Complete and test the client code (Example 15), and ensure it works under exceptional circumstances.
The namespace
defined in Example 12 contains the main class definitions.
Example 12. src/exceptions/registrar/registrar.h
[ . . . . ] #include "exceptions.h" #include <QStringList> namespace Registrar_Namespace { class Student { public: Student(const QString& name); long getNumber() const; QString getName() const; // other members as needed ... private: long m_Number;QString m_Name; static long nextNumber();
}; class Registrar { public: static Registrar& instance(); void insert(const Student& stu) throw (DupNumberException); void insert(const QString& name); void remove(const Student& stu) throw (NoStudentException); void remove(const long num) throw (NoNumberException); bool isInList(const Student& stu) const; bool isInList(const QString& name) const; QStringList report(QString name="all"); // other members as needed private: Registrar() {}; Registrar(const Registrar&);
Registrar& operator=(const Registrar&); QList<Student> m_List; }; } [ . . . . ]
In Example 13, we added some exceptions to the same namespace
from another header file.
Example 13. src/exceptions/registrar/exceptions.h
#ifndef EXCEPTIONS_H #define EXCEPTIONS_H #include <QString> namespace Registrar_Namespace { class Exception { public: Exception (const QString& reason); virtual ~Exception () { } QString what () const; private: QString m_Reason; }; class NoNumberException : public Exception { public: NoNumberException(const QString& reason); }; class NoStudentException : public Exception { public: NoStudentException(const QString& reason); }; class DupNumberException : public Exception { public: DupNumberException(const QString& reason); }; } #endif // #ifndef EXCEPTIONS_H
Part of the implementation file is provided to you in Example 14, to help you get started on the exercises.
Example 14. src/exceptions/registrar/registrar.cpp
/* Selected implementation examples This file is not complete! */ #include "registrar.h" namespace Registrar_Namespace { long Student::nextNumber() {static long number = 1000000; return ++number; } Registrar& Registrar::instance() {
static Registrar onlyInstance; return onlyInstance; } Exception::Exception(const QString& reason) : m_Reason(reason) {} QString Exception::what() const { return m_Reason; } NoNumberException::NoNumberException(const QString& reason) : Exception(reason) {} NoStudentException::NoStudentException(const QString& reason) : Exception(reason) {} DupNumberException::DupNumberException(const QString& reason) : Exception(reason) {} }
Example 15 contains some some client code to test these classes.
Example 15. src/exceptions/registrar/registrarClientCode.cpp
#include "registrar.h" #include <QDebug> int main() { using namespace Registrar_Namespace; Registrar& reg = Registrar::instance(); while (1) { try { reg.insert("George"); reg.insert("Peter"); reg.insert("Mildred"); Student s("George"); reg.insert(s); reg.remove(1000004); reg.remove(1000004); reg.remove(s); QStringList report = reg.report(); qDebug() << report.join("\n"); } catch (NoStudentException& nse) { qDebug() << nse.what() ; } catch (NoNumberException& nne) { qDebug() << nne.what() ; } catch (DupNumberException& dne) { qDebug() << dne.what() ; } } }
[Sutter2001] “To New Perchance To... Part 2”. May 2001. C/C++ Users Journal .
Generated: July 25 2011 | © 2011 Paul Ezust and Alan Ezust. |