Unexpected Exceptions

Unexpected Exceptions

Careless use of exception specifications can lead to calls to unexpected().

Such calls can be avoided through careful organization of exceptions and specifications.

For example, a well-defined subsystem, say DBAccess, will often have all its exceptions be derived from a class DBAccessException:

class DBAccessException 
{ 
  /* ... */ 
};

class DBConstraintViolation : public DBAccessException
{ 
  /* ... */ 
};

which can be thrown from a function:

void getDBData()

for instance.

Alternatively, calls to unexpected() can be intercepted by using the set_unexpected() call.

For example:

//
//  main.cpp
//  Intercepting Unexpected Exceptions
//
//  Created by Bryan Higgs on 10/7/24.
//

#include <iostream>
#include <exception>

// Exception classes
class HissyFit {};
class Up {};

void my_unexpected_handler() 
{
  std::cerr << "Unexpected exception occurred!\n";
  
  exit(1);
}

void doThat()
{
  std::cerr << "In doThat()...\n";
  throw 42;
}

void doThis( int i ) throw (HissyFit, Up)
{
  std::cerr << "In doThis()...\n";

  switch (i)
  {
    case 1:
      throw HissyFit();
    case 2:
      throw Up();
  }
  
  std::cerr << "In doThat() after switch statement...\n";

  doThat();
    // Throws an exception not in the doThis exception specification
}

int main(int argc, const char * argv[]) 
{
  std::set_unexpected( my_unexpected_handler );

  for (int i = 1; i <=3; i++)
  {
    try
    {
      doThis(i);
    }
    catch (HissyFit e)
    {
      std::cerr << "Caught HissyFit\n";
    }
    catch (Up)
    {
      std::cerr << "Caught Up\n";
    }
    catch (...)
    {
      std::exception_ptr eptr = std::current_exception();
      try
      {
        if (eptr)
        {
          std::rethrow_exception(eptr);
        }
      }
      catch (const std::exception& e)
      {
        std::cerr << "Caught exception: " << e.what() << std::endl;
      }
    }
  }
  
  return 0;
}

… which outputs the following:

In doThis()...
Caught HissyFit
In doThis()...
Caught Up
In doThis()...
In doThat() after switch statement...
In doThat()...
Unexpected exception occurred!
Program ended with exit code: 1

However…

The above program worked in C++11. Unfortunately, in C++17, the exception specifications were deprecated, which meant that the set_unexpected() function is also deprecated.


See what happens with the above program in C++17:

//
//  main.cpp
//  Intercepting Unexpected Exceptions
//
//  Created by Bryan Higgs on 10/7/24.
//

#include <iostream>
#include <exception>

// Exception classes
class HissyFit {};
class Up {};

void my_unexpected_handler() 
{
  std::cerr << "Unexpected exception occurred!\n";
  
  exit(1);
}

void doThat()
{
  std::cerr << "In doThat()...\n";
  throw 42;
}

void doThis( int i ) throw (HissyFit, Up)
{
  std::cerr << "In doThis()...\n";

  switch (i)
  {
    case 1:
      throw HissyFit();
    case 2:
      throw Up();
  }
  
  std::cerr << "In doThat() after switch statement...\n";

  doThat();
    // Throws an exception not in the doThis exception specification
}

int main(int argc, const char * argv[]) 
{
  std::set_unexpected( my_unexpected_handler );

  for (int i = 1; i <=3; i++)
  {
    try
    {
      doThis(i);
    }
    catch (HissyFit e)
    {
      std::cerr << "Caught HissyFit\n";
    }
    catch (Up)
    {
      std::cerr << "Caught Up\n";
    }
    catch (...)
    {
      std::exception_ptr eptr = std::current_exception();
      try
      {
        if (eptr)
        {
          std::rethrow_exception(eptr);
        }
      }
      catch (const std::exception& e)
      {
        std::cerr << "Caught exception: " << e.what() << std::endl;
      }
    }
  }
  
  return 0;
}

… which produces compilte-time errors:

main.cpp:28:22 ISO C++17 does not allow dynamic exception specifications

main.cpp:48:8 No type named 'set_unexpected' in namespace 'std'

Uncaught Exceptions & terminate()

If an exception is thrown, but not caught by an exception handler, the function terminate() will be called.

terminate() will also be called:

  • if the exception handling mechanism finds the stack is corrupted
  • when a destructor called during stack unwinding caused by an exception tries to exit using an exception.

terminate() exists because sometimes it is necessary to abandon exception handling for “less subtle error handling techniques”.

terminate() is intended to be a drastic measure, applied when the error recovery strategy implemented using exception handling has failed.

By default, terminate() calls abort()

A call to terminate() is assumed not to return to its caller.

You can redefine what it means to terminate by using the set_terminate() function:

For example:

//
//  Terminate.cpp
//  Intercepting Terminating Exceptions
//
//  Created by Bryan Higgs on 10/8/24.
//

#include <iostream>
#include <exception>

void MyTerminator()
{
  std::cerr << "In MyTerminator()..." << std::endl;

  // Do something before aborting...

  abort();
}

class Wrong
{
public:
  class WrongException {};
  
  void Ouch()
  {
    std::cerr << "In Wrong::Ouch()\n";
    throw WrongException();
  }
  
  ~Wrong()
  {
    std::cerr << "In Destructor for Wrong\n";
    throw "Random"; // string
  }
  
};

int main(int argc, const char * argv[])
{
  try 
  {
    Wrong wrong;
    wrong.Ouch();
  }
  catch (...)
  {
    std::cerr << "Caught (...)\n";
  }
  return 0;
}

… which outputs:

In Wrong::Ouch()
In Destructor for Wrong
libc++abi: terminating due to uncaught exception of type char const*

This example intends to show the behavior for both when a function throws an uncaught exception, and when a destructor throws an exception.

Note that the system produces a compile-time warning:

Terminate.cpp:34:5 '~Wrong' has a non-throwing exception specification but can still throw

which indicates that, while a destructor can throw an exception, it is considered poor practice.

Index