We explored some basic error detection and avoidance strategies in a previous chapter. But those strategies were limited, and we concluded we needed a technique that programmers can't inadvertently overlook or intentionally ignore. No programming construct can prevent programmers from ignorantly or maliciously writing bad code. Nevertheless, exception handling provides programmers with a powerful tool for managing potential program failures.
Exceptions were not included in C++'s initial release1 and were an "experimental" feature in the book2 that became the ANSI base document for the language. The history of exceptions in C++ explains why, despite a significant evolution, they are less refined than in other languages like Java and C#. The lack of refinement notwithstanding, C++ exceptions are flexible and satisfy the previously established needs.
| Application or Using Code | Supplier or Library Function |
|---|---|
try
{
function(...);
}
catch (exception)
{
...
} |
void function(...) noexcept(false)
{
...
if (...)
{
throw exception;
}
} |
In our previous exploration of basic error detection and avoidance strategies, we saw that library functions could return error codes or store them in global variables. But we also saw that it is impossible to force application programmers to check the codes, meaning that a program can experience an error without responding to it. Programs that experience an error but continue running in a compromised condition can corrupt data or damage hardware. Minimally, compromised programs waste the user's time and the system's resources as they work on tasks they cannot complete. Exceptions allow library function programmers to implement a fail-fast strategy that may not be graceful, but it prevents programs from continuing to run in a compromised state.
| Header File | Library Source Code File | Application Program File |
|---|---|---|
void function() noexcept(false); |
void function() noexcept(false)
{
cout << "Entering function" << endl;
throw "Exception";
cout << "Exiting function" << endl;
}
|
int main()
{
cout << "Starting main" << endl;
function();
cout << "main ending" << endl;
return 0;
} |
A program's flow of control is the sequence of operations or instructions that it runs. Previously, we employed an analogy to help us understand the flow of control. We imagined printing the program on a long, continuous strip of paper, placing a marker at the function main, and moving the marker from statement to statement as the program "runs." The line traced by the marker shows the branches the program follows and the loops of the iterative statements. The line breaks, begins anew, ends, and resumes with each function call. Understanding how exceptions impact a program's flow of control is essential for using them effectively.
Throw statements always interrupt a program's normal flow of control. Programs skip statements following a throw statement and a function call that throws an exception. As described in the previous figure, the program delivers uncaught exceptions to the runtime system, which aborts the program.
![]() |
![]() |
| (a) | (b) |
Programmers generally implement exceptions as objects and handle them in object-oriented programs. But, in Chapter 6, I claimed that we could use fundamental or primitive data types as exceptions and use them in non-object-oriented programs. Some detailed examples at the end of the chapter will use the simple exception handling illustrated in the following figure.
| Application | Library |
|---|---|
#include <iostream>
using namespace std;
void function1(bool) noexcept(false); // (a)
void function2(bool) noexcept(false);
void function3(bool) noexcept(false);
void function4(int) noexcept(false);
int main()
{
cout << "Starting main" << endl;
try
{
function1(true); // (b)
function2(true);
function3(true);
function4(7);
}
catch (char code) // (c)
{
cerr << "Exception: " << code << endl;
}
catch (int code) // (d)
{
cerr << "Exception: " << code << endl;
}
catch (const char* message) // (e)
{
cerr << "Exception: " << message << endl;
}
cout << "main ending" << endl;
return 0;
}
|
void function1(bool activate) noexcept(false)
{
cout << "Entering function1" << endl;
if (activate)
throw 'X'; // (c)
cout << "Exiting function1" << endl;
}
void function2(bool activate) noexcept(false)
{
cout << "Entering function2" << endl;
if (activate)
throw 7; // (d)
cout << "Exiting function2" << endl;
}
void function3(bool activate) noexcept(false)
{
cout << "Entering function3" << endl;
if (activate)
throw "Error in function3"; // (e)
cout << "Exiting function3" << endl;
}
void function4(int input) noexcept(false) // (f)
{
cout << "Entering function4" << endl;
switch (input % 2)
{
case 0:
throw "input is even";
case 1:
throw "input is odd";
}
cout << "Exiting function4" << endl;
} |
Stacks are a common data structure used throughout general computing. Their ubiquity makes them a good candidate for inclusion in data structure libraries such as C++'s Standard Template Library and Java's Collections Framework. Their simplicity also makes them good programming examples. push and pop are the principal stack operations. If we implement a stack as a fixed-size array, what happens if we try to push too many data items onto the stack, exceeding its size? Or what happens if we try to pop data off an empty stack?
Stacks save and organize data while a program runs. The program risks losing the data if a push operation fails to save data because the stack is full. If a pop operation fails to return valid data because the stack is empty, the program may continue running in a compromised state. Secure, library-grade functions test for these boundary conditions and respond appropriately. While the last versions of the push and pop functions did test overflow and underflow and printed corresponding error messages, they didn't prevent the program from running in an insecure state. We're in a position now to make the stack class, particularly the push and pop functions, more secure and robust.
#include <iostream>
using namespace std;
class StackException
{
private:
const char* message = nullptr;
int size = 0;
public:
StackException(const char* m) : message(m) {}
StackException(const char* m, int s) : message(m), size(s) {}
void display()
{
cerr << message << "\nstack size: " << size << endl;
}
};
|
|
class Stack
{
private:
int size;
int sp = 0;
char* stack = nullptr;
public:
Stack(int s) : size(s), stack(new char[size]) {}
void push(char c) noexcept(false)
{
if (sp < size)
stack[sp++] = c;
else
throw new StackException("Stack Overflow", size);
}
char pop() noexcept(false)
{
if (sp > 0)
return stack[--sp];
else
throw new StackException("Stack Underflow");
}
}; |
int main()
{
try
{
Stack s(3);
s.push('A');
s.push('B');
s.push('C');
s.push('D'); // overflow
s.pop();
s.pop();
s.pop();
s.pop(); // underflow
}
catch (StackException* se)
{
se->display();
}
return 0;
}
|
#include <iostream>
using namespace std;
class underflow
{
public:
void display() { cerr << "Stack underflow error" << endl; }
}; |
class overflow
{
private:
int size = 0;
public:
overflow(int s) : size(s) {}
void display() { cerr << "Stack overflow error"
"\nstack size: " << size << endl; }
}; |
class Stack
{
private:
int size;
int sp = 0;
char* stack = nullptr;
public:
Stack(int s) : size(s), stack(new char[size]) {}
void push(char c) noexcept(false)
{
if (sp < size)
stack[sp++] = c;
else
throw overflow(size);
}
char pop() noexcept(false)
{
if (sp > 0)
return stack[--sp];
else
throw underflow();
}
}; |
int main()
{
try
{
Stack s(3);
s.push('A');
s.push('B');
s.push('C');
s.push('D'); // overflow
s.pop();
s.pop();
s.pop();
s.pop(); // underflow
}
catch (overflow of)
{
of.display();
}
catch (underflow uf)
{
uf.display();
}
return 0;
} |
The choice between one or two exception classes is usually a matter of personal taste and programming style, but occasionally, the problem might favor one over the other. Exceptions and how we use them in programs can be more sophisticated than the examples. We must expand our understanding of object-oriented programming before considering their additional features. We'll revisit exception handling in subsequent chapters.
All programs are formatted with 4-character indentation.