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 (but it’s useful to have specific exception classes).
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