Exceptions

Exceptions

What is the Problem?

Classes have their public interfaces, which traditionally have simply specified what happens under normal circumstances.

But it’s important to define how things work under exceptional conditions, as well.

Traditionally, C programmers have used a large set of inconsistent approaches towards indicating success or failure. Often, by returning special values from a function call – but the types of values vary all over the place!

For example:

FILE *fp = fopen("myfile.dat", "r");
if (fp == NULL)
  fprintf(stderr, "Failed to open file\n");

or:

int ch = getchar();
if (ch == EOF)
  exit(0);

and so on…

But this approach has problems:

  • It’s difficult to find an appropriate return code value (EOF, NULL, -1, 0, … ) for a function (e.g. atoi())
  • Can produce problems with types (e.g. getchar())
  • A single return code needs to be augmented (e.g. errno)
  • Checking for exceptional conditions tends to obscure the algorithm for the normal case, and is error-prone.
  • Sometimes the code for error checking consumes more space than the normal code.
  • Most Important:  It’s much too easy for programmers (even well-intentioned ones) to ‘forget’ to check the return code.

Exceptions: A Solution

For these reasons, some languages, such as Ada, have a feature called exception handling:

declare
  LOW_FLUID_LEVEL : exception
begin
  RUN_ENGINE; -- a procedure call
exception
  when LOW_FLUID_LEVEL =>
    OPEN_VALVE;
    SOUND_ALARM;
  when NNUMERIC_ERROR =>
    CLOSE_VALVE;
    raise;
  when others =>
    LOG_UNKNOWN_ERROR;
end;

Exceptions are not necessarily errors

Note that exceptions are not necessarily errors – they are exceptional or unusual conditions that are not handled in the normal in-line code.

In Ada, exceptions are a built-in type:

  • an exception must be declared before it may be used (some are predeclared, built into the compiler)
  • an exception may be raised (explicitly or implicitly)
  • an exception may be handled by one or more exception handlers
  • An exception has scope, like any other declaration

Nested Exception Handling

Exceptions may be raised in inner scopes, and handled in outer scopes.

Here, in nested blocks (again, in Ada):

procedure MAIN is
begin
  ...
  declare
    LOCAL_ERROR : exception; -- exception is a type
  begin
    ...
  exception    -- an exception block
    when LOCAL_ERROR =>
      DO_SOMETHING;
  end;
  ...
exception -- another exception block
  when CONSTRAINT_ERROR =>
    DO_SOMETHING_ELSE;
  when NUMERIC_ERROR =>
    DO_SOMETHING_MORE;
end MAIN;

LOCAL_ERROR is handled by the exception handler in the inner block, but built-in exceptions CONSTRAINT_ERROR and NUMERIC_ERROR are handled in the outer block.

Exceptions in Nested Functions

Here, in nested functions (Ada, again):

procedure MAIN is
  ...
  type SMALL is digits 5 range 0.0..10.0;
  function INVERSE(I : float) return SMALL is
  begin
    return SMALL(1.0/I);
  exception
    when NUMERIC_ERROR =>
      return 10.0;
  end INVERSE;
  ...
begin
  ...
  Y := INVERSE(X);
  ...
exception
  when CONSTRAINT_ERROR =>
    DO_SOMETHING;
end MAIN;

If we call INVERSE(0.0), the division by zero raises an exception NUMERIC_ERROR, which is handled locally and converted to a return value of 10.0

If we call INVERSE(0.0001), the resulting value is not within the range of a SMALL, and when we attempt to return it, a CONSTRAINT_ERROR is raised, which is handled outside the function.

Exception Handling Choices

With exceptions, you have the following choices:

  • Handle the exception
  • Don’t handle it – in this case, the program will exit abnormally, when the exception handling facility fails to find an exception handler for the exception.

When you decide to handle the exception, you have the following choices:

  • Abandon the program (last resort – not user-friendly!)
  • Try the operation again (e.g. when interacting with a user)
  • Use an alternative approach (e.g. have extra redundancy in the program to handle such conditions)
  • Repair the cause of the error and try again.

Without exceptions, you can ignore a problem (intentionally, or otherwise), but it’s not a good idea.

With exceptions, it’s not so easy to just ignore the problem.  If you really want to ignore an exception you have to do it explicitly. (Not only is this good practice, it helps the programmer keep track of things.)

A C (not C++) Exception Mechanism

While C does not have exceptions built-in, it is possible to add a reasonably efficient and portable (albeit somewhat crude) exception mechanism using macros and the setjmp/longjmp C Run-Time-Library functions (see here).

A very useful C exception mechanism was proposed by Eric S. Roberts in 1989:
Implementing Exceptions in C, by Eric S. Roberts, Digital Systems Research Center Report #40, 1989

Roberts’ approach allows the following (example in C):

#include "exception.h"
  ...
exception OpenError; // exception is a type
  ...
TRY                 // TRY is a C macro
  ...
  file = OpenFile("test.dat", "r");
  ...
EXCEPT(OpenError)  // EXCEPT and ENDTRY are C macros
  fprintf(stderr, "Cannot open file 'test.dat'\n");
ENDTRY

The general form of a TRYENDTRY block is:

TRY
  statements
EXCEPT(exception-name-1)
  statements
EXCEPT(exception-name-2)
  statements
...
ENDTRY

An exception is raised by:

RAISE(exception-name, value);

where the value is an int to be communicated back to the handler, to provide additional information about the exception.

The TRY-FINALLY Block, and Resource Acquisition

This approach also included the option of a finally block (again, in C):

Roberts also specified a TRYFINALLY block, which is of the form:

TRY
  statements
FINALLY
  statements
ENDTRY

The major point of a FINALLY block is to allow for convenient cleanup in all cases:

#include "exception.h"
  ...
Acquire(Resource);
TRY
  /* code for accessing the resource */
FINALLY // FINALLY is a C macro
  /* this is executed whether or not an exception occurs */
  Release(Resource);
ENDTRY

This allows for acquired resources to be released, whether the access is successful or not

setjmp, longjmp and jmp_buf

Roberts’ exception handling mechanism made use of the setjmp and longjmp functions, which use a buffer called jmp_buf.

Function setjmp:

#include <setjmp.h>
...
int setjmp(jmp_buf env);

saves the current ‘environment’ in its jmp_buf argument, and returns 0.

Function longjmp:

#include <setjmp.h>
...
void longjmp(jmp_buf env, int val);

restores the environment saved by setjmp, which causes a return from the setjmp call with a value of val. longjmp never itself returns!

Type jmp_buf is a system-specific type that is of sufficient size to save the ‘environment’ (such as machine registers, etc.) so that the machine state may be saved, and later restored.  (It must be an array type, so that a pointer to it is always passed to a function.)

For example:

//
//  setjmpTest.c
//  setjmp & longjmp
//
//  Created by Bryan Higgs on 9/23/24.
//

#include <stdio.h>
#include <setjmp.h>

jmp_buf main_env;

int InputNumber(void)
{
  int n, ret;
  fflush(stdin);  // ensure there's nothing left
                  // from the last scanf
  ret = scanf("%d", &n);
                  // read an integer from stdin
  if (ret <= 0)
  {
    // something went wrong during the read...
    printf("Return value from scanf = %d ", ret);
    longjmp(main_env, 1);
  }
  return n;
}

int main()
{
  int n;
  if (setjmp(main_env) != 0)
    printf("\nInvalid, try again...\n");
  printf("Enter a valid number:");
  n = InputNumber();
  printf("You entered the number: %d\n", n);
  return 0;
}

Here’s the user’s (my) interaction with the program:

Enter a valid number:x
Return value from scanf = 0 
Invalid, try again...
Enter a valid number:52
You entered the number: 52
Program ended with exit code: 0

See if you can figure out how the program works!

Note that longjmp does not clean up any resources that may have been allocated since the setjmpThat is the programmer’s responsibility!

Well, enough already of Ada and C implementations. Let’s see what C++ implements:

C++ Exceptions

In C++:

  • Exceptions are built into the language, not added on via macros.
  • In C++, raising an exception is known as throwing an exception.
  • There is no special exception declaration;  you can throw any type.

For example:

//
//  Rational.h
//  C++ Exceptions
//
//  Created by Bryan Higgs on 9/23/24.
//

#ifndef Rational_h
#define Rational_h

class Rational
{
public:
  // Nested class
  class ZeroDenominatorException
  {};

  Rational(int numerator = 0, int denominator = 1)
    : m_num(numerator), m_den(denominator)
  {
    if (denominator == 0)
      throw ZeroDenominatorException();
    Normalize();
  }

private:
  void Normalize();

  int m_num, m_den;
};

#endif /* Rational_h */
//
//  Rational.cpp
//  C++ Exceptions
//
//  Created by Bryan Higgs on 9/23/24.
//

#include "Rational.h"

void Rational::Normalize()
{
  // Pretend to normalize the rational
}
//
//  main.cpp
//  C++ Exceptions
//
//  Created by Bryan Higgs on 9/23/24.
//

#include <iostream>
#include "Rational.h"

int main(int argc, const char * argv[]) 
{
  Rational r(4, 0);
    // Exception occurs! What happens?
  return 0;
}

Here’s what happens;

libc++abi: terminating due to uncaught exception of type Rational::ZeroDenominatorException

Catching Exceptions

Handling an exception is known in C++ as catching an exception.

The TRY in the C pseudo-exception handling becomes try in C++. 

For example:

//
//  main.cpp
//  C++ Exceptions
//
//  Created by Bryan Higgs on 9/23/24.
//

#include <iostream>
#include "Rational.h"

int main(int argc, const char * argv[]) 
{
  try
  {
    Rational r(4, 0);
  }
  catch (Rational::ZeroDenominatorException e)
  {
    std::cerr << "Caught a ZeroDenominatorException" << std::endl;
    return 1;
  }

  return 0;
}

Now, the program outputs:

Caught a ZeroDenominatorException
Program ended with exit code: 1

The catch construct is called an exception handler (or sometimes a catch handler)

A try block may contain several exception handlers, to catch more than one kind of exception:

try
{
	/* statements... */
}
catch(Rational::ZeroDenominatorException e)
{
	/* statements... */
}
catch(Vector::OutOfRangeException e)
{
	/* statements... */
}
catch(SomeOtherExceptionType e)
{
	/* statements... */
}

Throwing Exceptions

When an exception is thrown, the call stack is searched, from the throw point up through its callers, for an exception handler that handles the type of the exception being thrown.

If a matching exception handler is found, the call stack is “unwound” to the stack frame of the function that contains that handler, and destructors for local objects on each intervening stack frame are invoked along the way.

If a function throws an exception for which there is no matching exception handler on the call chain leading to the function throwing the exception, the default action is to terminate the program.

Once a handler has caught an exception, that exception is considered dealt with, and the presence of other handlers that might handle this exception becomes irrelevant.  

For example:

//
//  main.cpp
//  Try
//
//  Created by Bryan Higgs on 9/23/24.
//

#include <iostream>

class Vector
{
  // ...
public: class Range {};
  // ...
};

void DoSomethingSafely(Vector& vector)
{
  try {
    // do work on the Vector...
    throw Vector::Range();
  }
  catch (Vector::Range r) 
  {
    std::clog << "Caught Vector::Range exception "
              << "in DoSomethingSafely()" << std::endl;
  }
}

void BeSure(Vector &v)
{
  try 
  {
    DoSomethingSafely(v);
  }
  catch (Vector::Range r)
  {
    std::clog << "Caught Vector::Range exception "
              << "in BeSure()" << std::endl;
  }
}

int main(int argc, const char * argv[]) 
{
  Vector v;
  BeSure(v);
  
  return 0;
}

The exception handler in the BeSure function will never be invoked, because the function DoSomethingSafely will always catch the Vector::Range exception.

The program outputs:

Caught Vector::Range exception in DoSomethingSafely()
Program ended with exit code: 0

Discrimination of Exceptions

Once a handler has completed its work, the next statement following all the catch handlers for the try block is executed:

cout << "Something ";
try
{
  doSomething(v);
  cout << "good ";
}
catch(Vector::Range)
{
  cout << "strange ";
}
catch(Vector::Size)
{
  cout << "weird "
}
cout << "happened" << endl;

An exception is considered “handled” immediately upon entry into its handler.  Thus, any exceptions thrown while executing a handler must be dealt with by callers of the try block.  

For example:

try 
{ 
  /* ... */ 
}
catch (InputOverFlow)
{
  // ...
  throw InputOverFlow();
}

does not cause an infinite loop.

Exception handlers may be nested:

try
{
  // something...
}
catch (NumericOverflow)
{
  try
  {
    // something complicated...
  }
  catch (BathOverflow)
  {
    // get wet...
  }
}

But:

“such nesting is rarely useful in human-written code, and is more often than not an indication of poor style”

Bjarne Stroustrup, The C++ Programming Language, 3rd Edition

Naming of Exceptions

An exception is caught by specifying its type.

However, what is thrown is not a type, but an object.  If we need to convey information from the throw point to the handler, we can do so by putting data into the object thrown.

To examine this data, the handler must give a name to the exception object:

try 
{ 
  doSomething(v); 
}
catch (Vector::Range r)
{
  std::cerr << "Bad index " << r.getIndex() << std::endl;
}

The construct after the catch: catch (Vector::Range r) is a declaration, similar to a formal argument declaration for a function.

Here’s an example:

//
//  Vector.h
//  Naming Exceptions
//
//  Created by Bryan Higgs on 9/24/24.
//

#ifndef Vector_h
#define Vector_h

#include <iostream>

class Vector
{
public:
  class OutOfRange
  {
  public:
    OutOfRange(int index) 
      : m_index(index)
    {}
    
    int getIndex() { return m_index; }
    
  private:
    int m_index;
  };
  
  Vector(size_t size)
    : m_data( new int[size] ), m_size(size)
  {
    // Initialize each array element
    // with the square of its index.
    for (int i = 0; i < size; i++)
    {
      m_data[i] = i * i;
    }
  }
  
  ~Vector()
  {
    if (m_data != NULL)
      delete[] m_data;
  }
  
  void Print() const
  {
    std::cout << "Vector contains: "
              << m_size << " elements"
              << std::endl;
    for (int i; i < m_size; i++)
    {
      std::cout << "[" << i << "]"
                << m_data[i] << std::endl;
    }
  }
  
  int &operator[](int i)
  {
    if (i < 0 || i >= m_size)
      throw OutOfRange(i);
    return m_data[i];
  }
  
  // ...
  
private:
  int *m_data;
  size_t m_size;
  
};

#endif /* Vector_h */
//
//  main.cpp
//  Naming Exceptions
//
//  Created by Bryan Higgs on 9/24/24.
//

#include <iostream>
#include "Vector.h"

int main(int argc, const char * argv[]) 
{
  Vector v(20);
  
  v.Print();
  
  try 
  {
    std::cout << "Contents of [5] = " << v[5] << std::endl;
    std::cout << "Contents of [20] = " << v[20] << std::endl;
  } 
  catch (Vector::OutOfRange e)
  {
    std::cerr << "index OutOfRange: "
              << e.getIndex() << std::endl;
  }
  
  return 0;
}

Which outputs:

Vector contains: 20 elements
[0]0
[1]1
[2]4
[3]9
[4]16
[5]25
[6]36
[7]49
[8]64
[9]81
[10]100
[11]121
[12]144
[13]169
[14]196
[15]225
[16]256
[17]289
[18]324
[19]361
Contents of [5] = 25
Contents of [20] = index OutOfRange: 20
Program ended with exit code: 0

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?

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.

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

Resource Acquisition

When a program acquires a resource (e.g. opens a file, allocates some memory, sets a lock, etc.), it is often very important for the future operation of the program that the resource is properly released.

For example, in normal C/C++, this is typically handled like:

void useFile(const char *filename)
{
  FILE *fp = fopen(filename, "w");
  // use fp
  fclose(fp);
}

but this is not safe when something goes wrong – for example, if the program fails to open the file, or an error occurs while accessing the file.

A better approach might be:

void useFile(const char *filename)
{
  FILE *fp = fopen(filename, "w");
  try
  {
    // use fp
  }
  catch(...)
  {
    fclose(f); // close on exception
    throw;
  }
  fclose(fp);	// close normally
}

This is better, but it is verbose, tedious, potentially expensive, and error-prone.

The general form of such a program looks like:

void acquire_and_release()
{
  // acquire resource 1
  // ...
  // acquire resource n
  // use resources
  // release resource n
  // ...
  // release resource 1
}

Note the typical release sequence is in the reverse order of acquisition.

This resembles the behavior of objects created by constructors and destroyed by destructors.

So we could design a class to solve the problem:

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

#include <stdio.h>

class FilePtr
{
public:
  FilePtr(const char *name, const char *access)
  { 
    fp = fopen(name, access);
  }
  FilePtr(FILE *f) : fp(f) {}
  ~FilePtr() { fclose(fp); }
	
  operator FILE *() { return fp; }
private:
  FILE *fp;
};

void useFile(const char *filename)
{
  FilePtr file(filename, "r");
  // use file...
  // Destructor for file will be called 
  // whether an exception is thrown or not.
}

int main(int argc, const char * argv[])
{
  useFile(argv[1]);
  return 0;
}

“Resource acquisition is initialization.” (RAII)

This technique is called “Resource acquisition is initializationor RAII.

It is considered to be a critical technique in C++. (See here for more details.)

An object is not considered (fully) constructed until its constructor has completed.  Then, and only then, will stack unwinding (as a result of throwing an exception) call the destructor for the object.

Consider a class whose constructor needs to acquire two resources, a file x and a lock y:

class Resources
{
public:
  Resources(const char *file, const char *lock)
    :	m_file(x),  // acquire 'file'
      m_lock(y)	  // acquire 'lock'
  {}
  // ...
private:
  FilePtr m_file;
  LockPtr m_lock;
};

If an exception occurs after file has been constructed, but before lock has been constructed, then the destructor for file will be invoked, but the destructor for lock will not.

When this simple model is adhered to, the author of the constructor need not write explicit exception handling code.

The most common resource is free store (memory).  For example:

class X
{
public:
  X(int size) { m_p = new int[size]; init(); }
  ~X() { delete [] m_p; }
  // ...
private:
  void init();
  int *m_p;
};

But this code leads to memory leaks when used with exceptions – if init() throws an exception, then the memory acquired will not be freed, because the object wasn’t completely constructed.

A safe version is:

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

#include <stdio.h>

class IntPtr
{
public:
  IntPtr(size_t size) : m_p( new int[size] ) {}
  ~IntPtr() { delete [] m_p; }
  operator int*() { return m_p; }
private:
  int *m_p;
};

class Z
{
public:
  Z(int size) : m_p(size) { init(); }
  ~Z() { delete [] m_p; }
  // ...
private:
  void init();
  IntPtr m_p;
};

“In the C++ standard library, RAII is pervasive: for example, memory (string, vector, map, unordered_map, etc.), files (ifstream, ofstream, etc.), threads (thread), locks (lock_guard, unique_lock, etc.), and general objects (through unique_ptr and shared_ptr). The result is implicit resource management that is invisible in common use and leads to low resource retention durations.

~ Bjarne Stroustrup. A Tour of C++ 3rd Edition.

Resource Exhaustion

What do we do when an attempt to acquire a resource fails?  Two choices:

  • Termination:  Abandon the computation and return to some caller.
    • Termination is in most cases far simpler, and allows a system to maintain a better separation of levels of abstraction.  (Note that termination means only termination of the computation, not necessarily the program.)
  • Resumption:  Ask the caller to fix the problem and continue.

In C++:

  • Resumption is supported by the function-call mechanism.
  • Termination is supported by the exception-handling mechanism.

new_handler()

Both resumption and termination can be shown from the implementation of the new operator:

// new.h
#include <stdexcept>

class bad_alloc : public exception
{
  //...
};

typedef void (*new_handler)();

// ...
// new.cpp
#include <stdlib.h>  // for malloc and size_t
#include <new>

// ...

new_handler _new_handler = 0;

void *operator new(size_t size)
{
  for (;;)
  {
    // try to find memory
    if (void *p = malloc(size))
    {  // succeeded
      return p;
    }	
    if (_new_handler == 0)
    {  // no handler; give up
      throw bad_alloc();
    }
	
    new_handler();
  }
}

// ...

If operator new() cannot find memory, it calls _new_handler().

  • If _new_handler() can supply enough memory for malloc, it can return.
  • If it can’t, the handler can’t simply return without causing an infinite loop.

The new-handler might throw an exception:

void my_new_handler()
{
	try_to_find_some_memory();
	if (found_some)
		return;
	throw std::bad_alloc();
}

The new_handler can do one of the following:

  • Make more memory available: It can try to free up memory or acquire more memory from the system.
  • Throw an exception: It can throw an exception, typically std::bad_alloc, to signal the failure.
  • Terminate the program: It can call std::terminate to terminate the program.

To set up a new-handler:

set_new_handler( &my_new_handler );

Here’s an example:

//
//  main.cpp
//  new_handler
//
//  Created by Bryan Higgs on 10/5/24.
//

#include <iostream>
#include <new>

bool try_to_allocate_memory()
{
  // ...
  std::cerr << "In try_to_allocate_memory\n";
  std::cerr << "Pretending to try...\n";
  std::cerr << "Returning failure...\n";
  return false;
}

void my_new_handler()
{
  bool found_some = try_to_allocate_memory();
  if (found_some)
    return;
  throw std::bad_alloc();
}

int main(int argc, const char * argv[]) 
{
  std::cerr << "Setting new handler\n";
  std::new_handler old_handler =
        std::set_new_handler( &my_new_handler );
          // Get address of current new handler
  try
  {
    // ... operations that use new ...
    while (true)
    {
      std::cerr << "Attampting to allocate a large array...\n";
      long* arr = new long[5'000'000'000'000];
          // Try to allocate a very large array
    }
    // ...
  }
  catch (std::bad_alloc)
  {
    // do something in response to failure...
    std::cerr << "In std::bad_alloc handler\n";
  }
  catch (...)
  {
    std::cerr << "In catch(...) handler\n";
    std::set_new_handler( old_handler );  // restore old handler
    throw;    // rethrow the exception
  }

  std::cerr << "Restoring handler\n";
  std::set_new_handler( old_handler );    // restore handler

  // ...

  return 0;
}

This program outputs:

Setting new handler
Attampting to allocate a large array...
Attampting to allocate a large array...
Attampting to allocate a large array...
In try_to_allocate_memory
Pretending to try...
Returning failure...
In std::bad_alloc handler
Restoring handler
Program ended with exit code: 0

Warning:

In a multithreaded environment, using std::set_new_handler can lead to a race condition.

Here’s why:

  • Multiple threads might simultaneously attempt to call std::set_new_handler to set their own new handler functions.
  • If this happens, the outcome depends on the order of execution, which can be unpredictable. This creates a classic race condition scenario.

Solution:

  • C++11 and later:The C++ standard guarantees that std::set_new_handler is thread-safe, meaning it handles the race condition internally.
  • Prior to C++11:You’ll need to implement your own synchronization mechanisms, such as a mutex, to protect the call to std::set_new_handler.
Index