Exceptions

Paul Ezust

Alan Ezust


Table of Contents

1. Exception Handling
2. Exception Types
3. throwing things around
4. try and catch
4.1. Exercises: try and catch
5. More about throw
6. Rethrown Exceptions
7. Exception Expressions
8. What happens if new fails?
8.1. set_new_handler() : Another Approach To new Failures
8.2. Using set_new_handler and bad_alloc
8.3. Checking for null: new(nothrow)
9. Exercise: Exceptions
Bibliography

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.

1. Exception Handling

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.
}

2. Exception Types

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() { 1
        return "Subscript out of range!";
    }
};
[ . . . . ]

1

matches base class virtual signature


In Section 3 we discuss function signatures like that of RangeError::what().

3.  throwing things around

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];   1
}
[ . . . . ]

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;
    }
}

1

new will throw bad_alloc if it fails.


4. try and catch

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) { 1
      cerr << message << endl;
      exit(1);
    }
    catch(int &n) {
      cout << "caught int " << n << endl;
    } 
    catch(double& d) { 2
        cout << "caught a double:" << d <<  endl;
    }
    catch( ... ) { 3
      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>  


1

Is const necessary here?

2

abstract parameter

3

default action to be taken


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];  1
        if (counter ==3) {
            Vector<int> d(2*m);
            for (int i = 0; i < 2*m; ++i)
                d[i] = i * 3;
            c = a + d;  2
        }
        if (counter == 4) {
            for (int i = 0; i < WASTERS; ++i) 3
                double* ptr = new double(BIGSIZE);
            Vector<int> d(100 * BIGSIZE);
            Vector<int> e(100 * BIGSIZE);  4
             for (int i = 0; i < BIGSIZE; ++i)
                 d[i] = 3 * i;
        }
    } 
    catch(BadSizeError& bse) { 5
        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);    6
    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> 



1

Expect RangeError to be thrown.

2

Expect SizeMatchError to be thrown.

3

Use up most of the available memory.

4

Expect bad_alloc to be thrown.

5

Always catch exception objects by reference.

6

Expect BadSizeError to be thrown.


Because we did not have a handler for the bad_alloc exception, the default handler was called.

4.1. Exercises: try and catch

Make some changes to Example 5 to better understand exceptions. Try the following experiments and write your observations.

  1. What happens if we omit/remove the default handler?

  2. What happens if we omit/remove one of the exception types from the throw() list in the member function header?



[3] An example would be having a catch(QObject&) before a catch(QWidget&). Because only one catch gets executed, and the QObject is more general than the QWidget, it makes no sense to have the catch(QWidget&) unless it appears before the catch(QObject&).

5.  More about throw

Syntactically, throw can be used in three ways:

throwexpression

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.

returnType functionName(arglist)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.

6.  Rethrown Exceptions

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(); 1
        }
        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.


1

foo exited with i and j destroyed


[Note]Note

Remember that we do not recommend throwing basic types such as int, float, and char. A single number or character conveys little information and does not explain itself to someone reading the code. We violate this rule in some of our examples only to keep them as simple and brief as possible.

7.  Exception Expressions

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);

[Important]Question

Notice that this is a temporary object that is being thrown, but the throw unwinds all stack objects until it gets a matching handler. How can this work?

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().

8. What happens if new fails?

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>




[Important]Question

Why were we able to reach the exception handling code in this example, without causing any thrashing?

8.1.  set_new_handler() : Another Approach To new Failures

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.

[Important]Question

What happens if the last command in the out_of_store() function is not exit()?

8.2.  Using set_new_handler and bad_alloc

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>




8.3. Checking for null: new(nothrow)

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."

9.  Exercise: Exceptions

  1. 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&)
    

  2. 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.

    1. There are function defintions missing from Example 14, which need to be defined before this application is built. Define the missing functions.

    2. 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; 1
            QString m_Name;
            static long nextNumber(); 2
        };
    
        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&); 3
            Registrar& operator=(const Registrar&);
            QList<Student> m_List;
    
        };
    }
    [ . . . . ]
    

    1

    student number

    2

    used by constructor

    3

    private constructors


    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() { 1
             static long number = 1000000;
             return ++number;
        }
    
        Registrar& Registrar::instance() { 2
            
            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) {}
    }
    

    1

    Without the above using declaration, this would be "long Registrar_Namespace::Student::nextNumber"

    2

    Implementation of Singleton factory method: this is the only way clients can create instances of this Registrar.


    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() ;
            }
        }
    }
    

Bibliography

C++ References

[Stroustrup97] The C++ Programming Language. Special Edition. Bjarne Stroustrup. 1997. Addison Wesley. 0-201-70073-5.

[meyers] Effective C++. Scott Meyers. 1999-2005. Addison Wesley Professional Software Series . 0321334876.

[Sutter2001] “To New Perchance To... Part 2”. Herb Sutter. May 2001. C/C++ Users Journal .



[1] Whatever can go wrong will go wrong.

[2] It is impossible to make anything foolproof because fools are too clever.