Exception Handling ------------------ - What happens when an error occurs in a program ? - How is the error handled? - Who handles it? - Can the program recover, or should it just die? - Types of errors - compile-time: caught by compiler - run-time: error that the system is not prepared or able to handle - cause the program to crash or report an error while running - logical: Give unexpected or unwanted program behavior - don't necessarily crash program - Run-time errors also called exceptions - Examples of run-time errors that can occur during program execution: - File I/O errors - Out of memory errors - Arithmetic overflow - Invalid parameters - Out-of-bounds array access - Invalid instructions We have these types of error handling in C: a) Error handling with assert ----------------------------- - The assert macro aborts a program when a program assertion is not met. #include <assert.h> static Node * search(Node *root, int value) { // programmer assumes that his code calls search with non-NULL root assert(root!= NULL); ... } - Use assert for debugging and for critical errors where program should be aborted. b) Error handling by checking return values of functions -------------------------------------------------------- - Assertions are too drastic for many errors - Aborting a word processer when a file can not be opened - In many languages, certain return values of functions indicate errors - For example, search() will return NULL - Consider file I/O (in C) int process_file(char *file) { FILE *fp; if ((fp = fopen(file, "r")) == NULL) { perror("program"); // file could not be opened return; } if (fread(buffer, size, 1, fp) < size) { perror("program"); // file could not be read } // process file contents in buffer if (fclose(fp) < 0) { perror("program"); // file could not be closed properly } } - Problems with above approach - Caller needs to check for each function error - If caller forgets to check, random errors can occur much later in program - For series of nested calls, error has to be checked at each level - Main logic and error logic are intertwined - No notion of grouping of errors - Handling a failed constructor - Constructors don't return values In C++ we have: c) Exception handling --------------------- - Introduction - Provides an alternative to the above error-checking style - C++ exceptions represent errors that occur during program execution - Exception handlers are pieces of code outside the main logic which `intercept' (and take care of) exceptions. - C++ exception mechanism uses three constructs - A throw statement which cause specific exceptions to be generated (thrown) - Takes one argument which can be of any data type - That argument is said to be thrown by the statement - A try block where an exception may be generated - A catch block where an exception generated within a related try block is handled - Catch statements specify the data type of exception which the block is intended to handle. Example for out-of-bound array ------------------------------ Attempts to show that in some cases when you catch an error you can fix it and continue instead of exiting the program. #include <iostream> class Array { int *A; int size; public: Array(int n) {A = new int[n]; size = n - 1;} ~Array() {cout << "Destructor" << endl; delete [] A;} int& operator[](int index); void resize(); void print() { for (int i = 0; i < size; i++) cout << A[i] << " " ;} }; int& Array::operator[](int index) { if (index < 0 || index > size) { throw(index); } return A[index]; } void Array::resize(){ delete [] A; cout << "double" << endl; A = new int[2*size]; size = 2*size; } int main() { Array a (10); int exception; int i, n = 10; do{ exception = 0; try{ for (i = 0; i < n+2; i++) { a[i] = i; } } catch (int i) { cout << "Out of bound index: " << i << endl; a.resize(); exception = 1; } } while (exception); a.print(); } - Exam-style question exercising exceptions in C++ Q: What is the Output of the following program ? #include <iostream> // MyException is a class. Exceptions can be a basic or user-defined data type class MyException { public: MyException(int code) { errcode = code; }; int code() { return errcode; }; private: int errcode; }; class A { public: void One(); }; void A::One(int code) { // Throw an exception of MyException type // exception object is returned by copy throw MyException(code); } void callOneCatch() { A a; A b; try { // enclose code that may throw an exception within try a.One(1); } catch (MyException o) { // catch an exception of type MyException cout << "Caught MyException: " << o.code() << endl; } } A: Output is Caught MyException: 1 d) More details about exceptions -------------------------------- 1) Stack unwinding - Destructors for local objects called automatically for the entire sequence of functions when an exception is thrown - a and b destroyed automatically in example above Q: What is the Output of the following program: void callOne() { A a; a.One(2); // code doesn't catch exception } int main() { callOneCatch(); // no exception thrown try { callOne(); // exception thrown two function levels below } catch (MyException o) { cout << "Caught MyException: " << o.code() << endl; } callOne(); // uncaught exception } A: Output: Caught MyException: 1 Caught MyException: 2 Aborted (core dumped) 2) Often try blocks have more than one statement and the catch statements can be chained try { // Program fragment which can generate multiple type of exceptions. stmt1; stmt2; stmt3; } catch (int e1) { // handle int exception // can use the value of e1 } catch (AnotherKindOfException e2) { // handle exception // can use the value of e2.field } catch ( ... ) { // default handler for exceptions // not caught by other catch blocks // no exception object available } - The first catch block whose argument matches the thrown exception is selected for execution - Last catch should be the default 3) Try blocks can be nested - See example given in Exceptions.tar.gz (trace.cc) 4) Exception classes can be inherited - Can be derived from the standard "exception class" - First put derived classes, then base classes in catch blocks - Errors can be grouped 5) bad_alloc exception returned by new() when using "#include <new>" (see the new_exception.cc file in Exceptions.tar.gz for an example.) 6) Problems with exceptions and pointers func() { int *a = new int; throw 5; delete a; } - Heap memory for a is lost - Solution - Requires code duplication in C++ func() { int *a = new int; try { throw 5; } catch (exception e) { delete a; } delete a; } Can catch and re-throw exception. Useful for dealing with delete. func() { int *a = new int; try { ... } catch (...) { delete a; throw; //rethrows the exception, will be caught and //handled } delete a; //this occurs if there is no error } 7) What happens in the case of run-time errors ocurring during the execution of constructors ? A constructor has no return value, so no error code can be checked upon return. Exception throwing comes handy. If an error occurs, we throw an exception. However, an object is not considered "constructed" until the constructor completes execution. An exception thrown from within the constructor means stack unwinding (as mentioned above). However, an exception thrown from within the constructor means that the destructor will not be triggered upon stack unwinding for the "partially constructed" object. Thus if the constructor had just allocated memory dynamically before failing, this generates a memory leak. Example (following the Array class again): class Array { int *A; int size; public: Array(int n); ~Array() {cout << "Destructor" << endl; delete [] A;} void initialize (int n); ... }; This is how you should write the constructor: Array :: Array (int n) { A = new int [n]; size = n - 1; try { initialize (n); // if this throws an error a memory leak may occur } catch (...) { //catch any errors delete [] A; //cleans up the leak A = NULL; throw; //rethrow exception } }