Last Updated:

Handling Exceptions in C++ | Examples

Handling Exceptions in C++

1. Error Handling

When a program is constructed from separate modules, and especially when these modules are in independently developed libraries, error handling should be divided into two parts:

  1. generating information about the occurrence of an erroneous situation that cannot be resolved locally;
  2. Handling errors found elsewhere.

The author of the library can detect errors at run time, but is usually unaware of what to do in this case. A library user can know what to do if errors occur, but will not be able to detect them otherwise, errors would be handled in the user's code, and their search would not be delegated to the library. To help solve such problems, the concept of exclusion has been introduced.

The fundamental idea is that a function that detects a problem but does not know how to solve it throws an exception in the hope that the function that caused it (directly or indirectly) can solve the problem. A function that wants to solve problems of this type can indicate that it catches such exceptions.

This style of error handling is preferable to many traditional techniques. Consider the alternatives. If a problem is detected that cannot be resolved locally, the feature can:

  1. Stop execution.
  2. Return a value that means "error";
  3. Return a valid value and leave the program in an abnormal state.

Option 1 – "stop execution" – is what happens by default when an exception is not caught. For most mistakes, we have to come up with something better. A library that certainly completes execution cannot be used in a program whose first requirement is reliability.

Option 2 – "return an error value" – is not always feasible because there is often no acceptable value. Even in cases where this approach is applicable, it is often inconvenient because the result of each call must be checked for an erroneous value. This can greatly increase the size of the program.

Option 3 — "return a valid value and leave the program in an abnormal state" — has the disadvantage that the calling function may not notice that the program is in an abnormal state.

The exception handling mechanism provides an alternative to traditional methods in cases where they are insufficient, not elegant, and error-prone. It provides a way to explicitly separate error-handling code from "regular" code, thus making the program more readable and better suited to various tools. The exception handling mechanism provides a more regular way to handle errors, making it easier to interact between individually written code snippets.

It should be understood that error handling remains a complex task and that the exception handling mechanism – despite being more formalized than alternative methods – is relatively less structured than the language tools that provide local execution control. The C++ exception handling mechanism provides a means for the programmer to handle errors in the place where they are most naturally handled by a given system structure. Exceptions make the complexity of error handling more apparent. However, exceptions are not the cause of this complexity.

The exception handling mechanism can be considered as an alternative to the mechanism of returning from a function. Therefore, it is legitimate to use exceptions that have nothing to do with errors. However, the original purpose of the exception handling mechanism is to handle errors and provide stability when errors occur.

2. Generation and interception of exceptions

Exception handling provides a way to transfer control and information to an indefinite point where a desire has been expressed to handle situations of a given type. Situations of any type can be thrown and caught, and a variety of situations that can be excited in a function can be listed.

The reaction will be triggered only if the excitation expression is executed within a controlled block or in functions called from that block.

The syntax of the control block is try { ... } <list of reactions>

The list of reactions is as follows: catch (<Advertence>) { ... } [ catch (<Reaction>) { ... } ... ]

The excitation expression has the following syntax:
throw <expression>;

When a situation is excited (i.e., the throw statement is executed), control is transferred to the reaction. The operand type of the throw operator determines which reactions can intercept a given situation.

If no suitable reaction is found among the reactions of the controlled unit, the search for a suitable reaction continues in the controlled unit encompassing the control unit.

If the program does not find a suitable response, the terminate() function is called. The terminate() function invokes the function specified the last time the set_terminate() function was accessed. By default, the function called from the terminate() function is abort().. A function called by the terminate() function must terminate the program.

An exception is an object of some class that is a representation of an exceptional case. The code that encounters the error generates a throw instruction object. The code snippet expresses its desire to handle the exception by using the catch statement. The result of throw throw throwing an exception is to unwind the stack until a suitable catch is found in the function that directly or indirectly called the function that generated the exception.

The very fact of generating an exception transmits information about the error and its type. In addition, the exception may contain additional information. The fundamental purpose of exception handling techniques is to transmit information for recovery after a problem has occurred and to do so in a reliable and convenient manner.

Example 1

 

FILE *open(char *fname)
 { FILE *f = fopen(fname, "r");
   if (!f) throw fname;
   return f;
  }

void main()
 { try
    { FILE *f1 = open("in1.txt"); 
      FILE *f2 = open("in2.txt");
     }
   catch (char *str)
    { printf("Impossible to open file '%s'!\n", str);
      return;
     }
   ...
  }

Example 2

class Ex1
 { private:
    int reason;
   public:
    Ex1(int r) : reason(r) { }
    int Reason() { return reason; }
  };

class Ex2 { };

void f1()
 { ...
   if (...) throw Ex1(0);
   if (...) throw Ex1(2);
   ...
   if (...) throw Ex2();
  }

void f2()
 { ...
   if (...) throw Ex2();
  }

void main()
 { try
    { ...
      f1();
      ...
      f2();
      ...
     }
   catch (Ex1 ex)
    { switch (ex.Reason())
       { case 0: ...
         case 1: ...
         case 2: ...
        }
     }
   catch (Ex2 ex)
    { ... }
  }

3. Catch exceptions

Let's look at an example.

try { throw E(); } catch (H) { ... }

The handler will be called if:

  1. H of the same type as E;
  2. H is a uniquely available public base class for E;
  3. H and E are pointers, and 1 or 2 is executed for the types they refer to;
  4. H is a reference, and 1 or 2 is executed for the type referenced by H.

4. Grouping of exceptions

Often, exceptions are naturally broken down into families. It follows that inheritance can be useful for structuring exceptions and helping to handle them. For example, exceptions for a math library can be organized as follows:

class MathErr { ... };
class Overflow     : public MathErr { ... };
class Underflow    : public MathErr { ... };
class ZeroDivision : public MathErr { ... }; 

This allows us to handle the exception of any class derived from MathErr without worrying about which exception occurred.

try
 { ... }
catch (Overflow)
 { ... }
catch (MathErr)
 { ... } 

Organizing exceptions in the form of hierarchies can be of great importance for the reliability of the code. If a new exception were introduced into the mathematical library, every piece of code that tries to handle all mathematical exceptions would be subject to modification. This requires a huge amount of work and is generally impossible.

5. Regeneration

By catching the exception, the handler may decide that it cannot fully handle the error. In this case, the handler does what it can, and then throws the exception again.


try
 { ... }
catch (MathErr)
 { if (...)
    ...
   else
    { ...
      throw;
     }
  }

The fact of re-generation is marked by the lack of operand in throw. If an attempt is made to regenerate in the absence of an exception, the terminate() function will be called.

A re-thrown exception is the initial exception, that is, even if the handler worked with a base class object, the enveloping block will receive the natively generated derived class object.

6. Catch all exceptions

In declaring a situation, you can specify an ellipsis for the catch handler that identifies with any situation. An ellipsis reaction, if any, should be last on the list of reactions of some control block. 

try
  { // Do something }
catch(...)
  { // Handling all exceptions }

7. How to Write Handlers

Because exceptions to derived classes can be intercepted by base class exception handlers, the order in which handlers are written in the try statement is important. Handlers are checked in the order in which they are written.


void f(Queue q)
 { try
    { for ( ; ; )      
       { int x = q.Get();
         ...
        }
     }
   catch (Queue::Empty)
    { return; }
  } 

8. Exceptions in constructors

Exceptions provide a way to resolve the problem by reporting an error from the constructor. Because the constructor does not return a value that the calling function could validate, the traditional (that is, without using exception handling) alternatives remain the following:

  1. Return the object in the "wrong" state and rely on the user to check its state.
  2. Assign a value to a nonlocal variable to indicate that the object was created unsuccessfully and rely on the user to validate it.
  3. Do not perform any initialization in the constructor and rely on the user to call the initialization function (which still needs to be written!) before the first use of the object.
  4. Mark an object as uninitialized and initialize the object when the class member function is first called (such a function may return an error message if initialization fails, but the user must again check the value returned by the function).

Exceptions allow you to pass information about unsuccessful initialization from the constructor. For example, the Vector class could protect itself from requesting too much memory by throwing an appropriate exception.

 class Vector { ... public: class Size { ... }; Vector(int n = 0); ... }; Vector::Vector(int n) { if (n < 0 || n > MAX_SIZE) throw Size(); ... }

The code that creates the vectors can now catch the Vector::Size error and try to do something meaningful.

9. Exceptions that are not errors

If an exception is expected and intercepted in a way that does not adversely affect the behavior of the program, why can it be considered an error? Exception handling mechanisms can be thought of as another control structure.


void f(Queue q)
 { try
    { for ( ; ; )      
       { int x = q.Get();
         ...
        }
     }
   catch (Queue::Empty)
    { return; }
  } 

Exception handling is less structured than local control structures, and exception handling is often less efficient if an exception is actually thrown. Therefore, exceptions should be used where traditional governing structures are not an inelegant solution or cannot be used.

Excessive use of exceptions leads to incomprehensible code. In general, you should take the view that "exception handling is error handling." With this approach, the code is clearly divided into two parts: ordinary code and error-handling code.

10. Exception Specification

Throwing and catching exceptions changes the way functions interact. Therefore, it may be useful to specify in the declaration a set of exceptions that can be generated by the function.


int f(int n) throw (ex1, ex2);

This declaration means that the function f can only throw ex1ex2, and exceptions that derive from those types, not others. The most important advantage is that the declaration of the function belongs to the interface that is seen by those who call it.

It is assumed that a function declared without an exception specification can throw any exception.


int g(int n);

A function that does not generate exceptions can be declared with an empty list of exception specifications.


int h(int n) throw();

11. Example. Stack class that throws exceptions

Modify the Stack class (from Lecture 11) so that when the stack overflows and an attempt to take an element from the empty stack, appropriate exceptions are thrown.

#include <cstdio>

class Stack;

class StackEmpty
 { private:
    Stack *stack;
   public:
    StackEmpty(Stack *p) : stack(p) { }
    Stack* GetPtr() { return stack; }
  };

class StackFull
 { private:
    Stack *stack;
    int n;
   public:
    StackFull(Stack *p, int i) : stack(p), n(i) { }
    Stack* GetPtr() { return stack; }
    int GetValue()  { return n; }
  };

class Stack
 { private:
    enum { SIZE = 100 };
    int stack[SIZE];
    int *cur;
   public:
    Stack()  { cur = stack; }
    ~Stack() { }
    int  Push(int n) throw (StackFull);
    int  Pop() throw (StackEmpty);
    int  IsEmpty() const { return cur == stack; }
    int  operator >> (int& s) { s = Pop(); return s; }
    int  operator << (int s)  { return Push(s);      }
  };

int Stack::Push(int n) throw (StackFull)
 { if (cur - stack < SIZE)
    { *cur++ = n; return n; }
   else
    throw StackFull(this, n);
  }

int Stack::Pop() throw (StackEmpty)
 { if (cur != stack)
    return *--cur;
   else
    throw StackEmpty(this);
  }

void main()
 { Stack s;
   int n;

   try
    { s << 1;
      s << 2;
      s << 3;
      s << 4;
      s << 5;
      s >> n;
      printf("%d\n", n);
      s >> n;
      printf("%d\n", n);
      s >> n;
      printf("%d\n", n);
      s >> n;
      printf("%d\n", n);
     }
   catch (StackFull s)
    { printf("Attempt to put a value %d to the full stack at the address %p\n", s.GetValue(), s.GetPtr()); }
   catch (StackEmpty s)
    { printf("Attempt to get a value from the empty stack at the address %p\n", s.GetPtr()); }
  }