Grouping of Exceptions

Grouping of Exceptions

Exceptions often fall naturally into families or groups.

For example, consider a Matherr exception that might be thrown by a standard library of math functions.

//
//  main.cpp
//  Grouping of Exceptions
//
//  Created by Bryan Higgs on 9/24/24.
//

enum Matherr {Overflow, Underflow, ZeroDivide};

int main(int argc, const char * argv[]) 
{
  try
  {
    // ...
  }
  catch (Matherr m)
  {
    switch (m)
    {
      case Overflow: /* ... */ break;
      case Underflow: /* ... */ break;
      case ZeroDivide: /* ... */ break;
    }
  }
    
  return 0;
}

but notice that this forces the handler to use a switch statement (or equivalent) to determine the actual exception thrown.

This should be a familiar situation.  We have already encountered situations where such switch statements occurred, and were considered problematic.

What did we use to solve this problem before?

Solution: Inheritance, and Virtual Functions!

It is possible to use inheritance and virtual functions to avoid the need for such a switch statement, and to improve the flexibility and maintainability of the code:

//
//  GroupByInheritance.cpp
//  Grouping of Exceptions
//
//  Created by Bryan Higgs on 9/24/24.
//

class Matherr { /* ... */ };
class Overflow: public Matherr { /* ... */ };
class Underflow: public Matherr { /* ... */ };
class ZeroDivide: public Matherr { /* ... */ };

int main(int argc, const char * argv[])
{
  try
  {
    // ...
  }
  catch (Overflow o)
  {
    // Handle Overflow or any subclass thereof...
  }
  catch (Matherr m)
  {
    // Handle any other Matherr...
  }
    
  return 0;
}

We use the fact that several classes are subclasses of (derived from) Matherr, and inheritance takes care of things in the catch blocks.

Organizing exceptions into such hierarchies can be important for code robustness.  To handle all exceptions from a class library, we would have to laboriously list each possible exception:

try 
{ 
  /* ... */ 
}
catch (Overflow) 
{ 
  /* ... */ 
}
catch (Underflow) 
{ 
  /* ... */ 
}
catch (ZeroDivide) 
{ 
  /* ... */ 
}
// ...

which is tedious and error-prone.

Also, when a class library is augmented to use additional exceptions, every piece of code that uses the library must be modified to catch the new exceptions and then must be recompiled.  Such changes are made with great difficulty and reluctance.

So, organizing exceptions into such hierarchies is a good approach, because it provides a mechanism to allow for library enhancement without the need for client code changes and recompilation.

We can extend this to declare exceptions that are members of more than one ‘group’:

class NetworkFileError: public NetworkError,
                        public FileSystemError
{ 
  /* ... */ 
};

So, NetworkFileError exceptions can be caught by functions concerned with network exceptions:

void NetFunction()
{
  try 
  { 
    /* something */ 
  }
  catch (NetworkError)
  { 
    /* ... */
  }
}

and by functions dealing with file system exceptions:

void FileSystemFunction()
{
  try 
  { 
    /* something */ 
  }
  catch (FileSystemError)
  { 
    /* ... */ 
  }
}

This is important because services (such as networking) can be transparent, such that the writer of FileSystemFunction might not even be aware that the network might be involved.

One could also make a policy of declaring a base class Exception, and deriving all exceptions from it.

There is indeed such a class (std::exception) in the C++ Standard Library.

See here.

Derived Exceptions

When using derived exceptions, the semantics for a handler catching and naming an exception are identical to that of a function accepting an argument.

  • That is, the formal argument is initialized with the argument value. 
  • If the exception type specified in the handler is a base type, then the actual thrown exception is “cut down” to the exception caught.
    For example:
//
//  Derived Exceptions.cpp
//  Grouping of Exceptions
//
//  Created by Bryan Higgs on 9/24/24.
//

#include <iostream>

class Matherr
{
public:
  virtual void debugPrint()
  {
    std::cout << "In Matherr::debugPrint()" 
              << std::endl;
  }
};

class IntegerOverflow: public Matherr
{
public:
  virtual void debugPrint()
  {
    std::cout << "In IntegerOverflow::debugPrint()"
              << std::endl;
  }
};

int main(int argc, const char * argv[])
{
  try
  {
    // ...
    throw IntegerOverflow();
    // ...
  }
  catch (Matherr m)
  {
    m.debugPrint(); // prints what?
  }
  return 0;
}

This program output:

In Matherr::debugPrint()
Program ended with exit code: 0

Do you understand why?

However, pointers or references can be used to avoid losing information:

//
//  Derived Exceptions.cpp
//  Grouping of Exceptions
//
//  Created by Bryan Higgs on 9/24/24.
//

#include <iostream>

class Matherr
{
public:
  virtual void debugPrint()
  {
    std::cout << "In Matherr::debugPrint()" 
              << std::endl;
  }
};

class IntegerOverflow: public Matherr
{
public:
  virtual void debugPrint()
  {
    std::cout << "In IntegerOverflow::debugPrint()"
              << std::endl;
  }
};

int main(int argc, const char * argv[])
{
  try
  {
    // ...
    throw IntegerOverflow();
    // ...
  }
  catch (Matherr &m) // Change to reference
  {
    m.debugPrint(); // prints what?
  }
  return 0;
}

Now, the program outputs:

In IntegerOverflow::debugPrint()
Program ended with exit code: 0

So the catch handler determines the actual type of the exception.

‘Rethrowing’ Exceptions

It is not uncommon for a handler to decide that it can do nothing about an exception.  In this case, the same exception can be thrown again, hoping some outer handler will be able to do better:

void doSomething()
{
  try 
  { 
    /* something */ 
  }
  catch (Matherr)
  {
    if (!canHandle)
      throw;  // re-throw caught exception
  }
}

Using throw without an argument is a ‘rethrow’. It throws the original exception that was thrown, not a “cut down” version.

Using throw without an argument is only allowed within a handler.

Catching Any Exception

There is a way to specify the handling of any exception:

void doSomeOtherThing()
{
  try 
  { 
    /* ... */ 
  }
  catch (...)		// catch any exception
  {
    // Cleanup, or whatever
    throw;
  }
}

The ellipsis (...) indicates any exception.

However, this often indicates lazy programming. It is much better to know what exceptions might be thrown, and handle them appropriately.

Catching Derived Exceptions

The order in which handlers are written is important. 

In general, handlers should be specified in order most specific to most general:

try 
{ 
  /* ... */ 
}
catch (InputBufferOverflow)
{ 
  /* Handle input buffer overflow */ 
}
catch (IOError)
{ 
  /* Handle any I/O error */ 
}
catch (std::exception)
{ 
  /* Handle any library exception */ 
}
catch (...)
{ 
  /* Handle any other exception */ 
}

The type in a handler matches:

  • if it directly refers to the exception thrown, or:
  • if it is of an accessible base class of that exception, or:
  • if the exception thrown is a pointer and the exception caught is a pointer to an accessible base class of that exception.

The compiler knows the class hierarchy, so it can issue warnings or errors for:

//
//  main.cpp
//  Derived Exceptions
//
//  Created by Bryan Higgs on 10/4/24.
//

class Matherr
{
  // ...
};

class IntegerOverflow: public Matherr
{
  // ...
};

int main(int argc, const char * argv[])
{
  try {
    /* ... */
  }
  catch (Matherr)   // masks IntegerOverflow
  {
    /* handle any Matherr */
  }
  catch (...)    // masks all other exceptions
  {
    /* handle any exception */
  }
  catch (IntegerOverflow)
  {
    /* handle IntegerOverflow exception */
    /* ****Never gets called**** */
  }

  return 0;
}

This produces a compile-time error:

main.cpp:27:3 Catch-all handler must come last

… and then, when we move the catch (...) to avoid this error:

//
//  main.cpp
//  Derived Exceptions
//
//  Created by Bryan Higgs on 10/4/24.
//

class Matherr
{
  // ...
};

class IntegerOverflow: public Matherr
{
  // ...
};

int main(int argc, const char * argv[])
{
  try {
    /* ... */
  }
  catch (Matherr)   // masks IntegerOverflow
  {
    /* handle any Matherr */
  }
  catch (IntegerOverflow)
  {
    /* handle IntegerOverflow exception */
    /* ****Never gets called**** */
  }
  catch (...)    // masks all other exceptions
  {
    /* handle any exception */
  }

  return 0;
}

This produces a compile-time warning:

main.cpp:27:10 Exception of type 'IntegerOverflow' will be caught by earlier handler

Index