No program exists in isolation; they all depend on external facilities such as interfaces with the operating system, networks and file systems, external code such as third-party libraries, and user input. Each of these areas can introduce situations that require you to respond to problems you may encounter. These potential problems can be referred to with the general term exceptional situations. Even perfectly written programs encounter errors and exceptional situations. Thus, anyone who writes a computer program must include error-handling capabilities. Some languages, such as C, do not include many specific language facilities for error handling. Programmers using these languages generally rely on return values from functions and other ad hoc approaches. Other languages, such as Java, enforce the use of a language feature called exceptions as an error-handling mechanism. C++ lies between these extremes. It provides language support for exceptions, but does not require their use. However, you can’t ignore exceptions entirely in C++ because a few basic facilities, such as memory allocation routines, use them.
Exceptions are a mechanism for a piece of code to notify another piece of code of an “exceptional” situation or error condition without progressing through the normal code paths. The code that encounters the error throws the exception, and the code that handles the exception catches it. Exceptions do not follow the fundamental rule of step-by-step execution to which you are accustomed. When a piece of code throws an exception, the program control immediately stops executing code step by step and transitions to the exception handler, which could be anywhere from the next line in the same function to several function calls up the stack. If you like sports analogies, you can think of the code that throws an exception as an outfielder throwing a baseball back to the infield, where the nearest infielder (closest exception handler) catches it. Figure 14-1 shows a hypothetical stack of three function calls. Function A()
has the exception handler. It calls function B()
, which calls function C()
, which throws the exception.
Figure 14-2 shows the handler catching the exception. The stack frames for C()
and B()
have been removed, leaving only A()
.
Most modern programming languages, such as C# and Java, have support for exceptions, so it’s no surprise that C++ has full-fledged support for them as well. However, if you are coming from C, then exceptions are something new; but once you get used to them, you probably won’t want to go back.
As mentioned earlier, run-time errors in programs are inevitable. Despite that fact, error handling in most C and C++ programs is messy and ad hoc. The de facto C error-handling standard, which was carried over into many C++ programs, uses integer function return codes, and the errno
macro to signify errors. Each thread has its own errno
value. errno
acts as a thread-local integer variable that functions can use to communicate errors back to calling functions.
Unfortunately, the integer return codes and errno
are used inconsistently. Some functions might choose to return 0 for success and -1 for an error. If they return -1, they also set errno
to an error code. Other functions return 0 for success and nonzero for an error, with the actual return value specifying the error code. These functions do not use errno
. Still others return 0 for failure instead of for success, presumably because 0 always evaluates to false
in C and C++.
These inconsistencies can cause problems because programmers encountering a new function often assume that its return codes are the same as other similar functions. That is not always true. For example, on Solaris 9, there are two different libraries of synchronization objects: the POSIX version and the Solaris version. The function to initialize a semaphore in the POSIX version is called sem_init()
, and the function to initialize a semaphore in the Solaris version is called sema_init()
. As if that weren’t confusing enough, the two functions handle error codes differently! sem_init()
returns -1 and sets errno
on error, while sema_init()
returns the error code directly as a positive integer, and does not set errno
.
Another problem is that the return type of functions in C++ can only be of one type, so if you need to return both an error and a value, you must find an alternative mechanism. One solution is to return an std::pair
or std::tuple
, an object that you can use to store two or more types. These classes are discussed in the upcoming chapters that cover the Standard Library. Another choice is to define your own struct
or class that contains several values, and return an instance of that struct
or class from your function. Yet another option is to return the value or error through a reference parameter or to make the error code one possible value of the return type, such as a nullptr
pointer. In all these solutions, the caller is responsible to explicitly check for any errors returned from the function and if it doesn’t handle the error itself, it should propagate the error to its caller. Unfortunately, this often results in the loss of critical details about the error.
C programmers may be familiar with a mechanism known as setjmp()/longjmp()
. This mechanism cannot be used correctly in C++, because it bypasses scoped destructors on the stack. You should avoid it at all costs, even in C programs; therefore, this book does not explain the details of how to use it.
Exceptions provide an easier, more consistent, and safer mechanism for error handling. There are several specific advantages of exceptions over the ad hoc approaches in C and C++.
[[nodiscard]]
attribute, introduced in Chapter 11, offers a possible solution to prevent return codes from being ignored, but it’s not foolproof either. Exceptions cannot be forgotten or ignored: if your program fails to catch an exception, it terminates.In some compilers (fewer and fewer these days), exception handling added a tiny amount of overhead to any function that had an exception handler. For most modern compilers there is a trade-off in that there is almost no overhead in the non-throwing case, and only some slight overhead when you actually throw something. This trade-off is not a bad thing because throwing an exception should be exceptional.
Exception handling is not enforced in C++. For example, in Java a function that does not specify a list of possible exceptions that it can throw is not allowed to throw any exceptions. In C++, it is just the opposite: a function can throw any exception it wants, unless it specifies that it will not throw any exceptions (using the noexcept
keyword)!
I recommend exceptions as a useful mechanism for error handling. I feel that the structure and error-handling formalization that exceptions provide outweigh the less desirable aspects. Thus, the remainder of this chapter focuses on exceptions. Also, many popular libraries, such as the Standard Library and Boost, use exceptions, so you need to be prepared to handle them.
Exceptional situations arise frequently in file input and output. The following is a function to open a file, read a list of integers from the file, and return the integers in an std::vector
data structure. The lack of error handling should jump out at you.
vector<int> readIntegerFile(string_view fileName)
{
ifstream inputStream(fileName.data());
// Read the integers one-by-one and add them to a vector.
vector<int> integers;
int temp;
while (inputStream >> temp) {
integers.push_back(temp);
}
return integers;
}
The following line keeps reading values from the ifstream
until the end of the file is reached or until an error occurs:
while (inputStream >> temp) {
If the >>
operator encounters an error, it sets the fail bit of the ifstream
object. In that case, the bool()
conversion operator returns false
and the while
loop terminates. Streams are discussed in more detail in Chapter 13.
You might use readIntegerFile()
like this:
const string fileName = "IntegerFile.txt";
vector<int> myInts = readIntegerFile(fileName);
for (const auto& element : myInts) {
cout << element << " ";
}
cout << endl;
The rest of this section shows you how to add error handling with exceptions, but first, we need to delve a bit deeper into how you throw and catch exceptions.
Using exceptions consists of providing two parts in your program: a try/catch
construct to handle an exception, and a throw
statement that throws an exception. Both must be present in some form to make exceptions work. However, in many cases, the throw
happens deep inside some library (including the C++ runtime) and the programmer never sees it, but still has to react to it using a try/catch
construct.
The try/catch
construct looks like this:
try {
// ... code which may result in an exception being thrown
} catch (exception-type1 exception-name) {
// ... code which responds to the exception of type 1
} catch (exception-type2 exception-name) {
// ... code which responds to the exception of type 2
}
// ... remaining code
The code that may result in an exception being thrown might contain a throw
directly. It might also be calling a function that either directly throws an exception or calls—by some unknown number of layers of calls—a function that throws an exception.
If no exception is thrown, the code in the catch
blocks is not executed, and the “remaining code” that follows will follow the last statement executed in the try
block.
If an exception is thrown, any code following the throw
or following the call that resulted in the throw
, is not executed; instead, control immediately goes to the right catch
block, depending on the type of the exception that is thrown.
If the catch
block does not do a control transfer—for example, by returning a value, throwing a new exception, or rethrowing the exception—then the “remaining code” is executed after the last statement of that catch
block.
The simplest example to demonstrate exception handling is avoiding division-by-zero. This example throws an exception of type std::invalid_argument
, which requires the <stdexcept>
header:
double SafeDivide(double num, double den)
{
if (den == 0)
throw invalid_argument("Divide by zero");
return num / den;
}
int main()
{
try {
cout << SafeDivide(5, 2) << endl;
cout << SafeDivide(10, 0) << endl;
cout << SafeDivide(3, 3) << endl;
} catch (const invalid_argument& e) {
cout << "Caught exception: " << e.what() << endl;
}
return 0;
}
The output is as follows:
2.5
Caught exception: Divide by zero
throw
is a keyword in C++, and is the only way to throw an exception. The invalid_argument()
part of the throw
line means that you are constructing a new object of type invalid_argument
to throw. It is one of the standard exceptions provided by the C++ Standard Library. All Standard Library exceptions form a hierarchy, which is discussed later in this chapter. Each class in the hierarchy supports a what()
method that returns a const
char*
string describing the exception1. This is the string you provide in the constructor of the exception.
Let’s go back to the readIntegerFile()
function. The most likely problem to occur is for the file open to fail. That’s a perfect situation for throwing an exception. This code throws an exception of type std::exception
, which requires the <exception>
header. The syntax looks like this:
vector<int> readIntegerFile(string_view fileName)
{
ifstream inputStream(fileName.data());
if (inputStream.fail()) {
// We failed to open the file: throw an exception
throw exception();
}
// Read the integers one-by-one and add them to a vector
vector<int> integers;
int temp;
while (inputStream >> temp) {
integers.push_back(temp);
}
return integers;
}
If the function fails to open the file and executes the throw exception();
line, the rest of the function is skipped, and control transitions to the nearest exception handler.
Throwing exceptions in your code is most useful when you also write code that handles them. Exception handling is a way to “try” a block of code, with another block of code designated to react to any problems that might occur. In the following main()
function, the catch
statement reacts to any exception of type exception
that was thrown within the try
block by printing an error message. If the try
block finishes without throwing an exception, the catch
block is skipped. You can think of try
/catch
blocks as glorified if
statements. If an exception is thrown in the try
block, execute the catch
block. Otherwise, skip it.
int main()
{
const string fileName = "IntegerFile.txt";
vector<int> myInts;
try {
myInts = readIntegerFile(fileName);
} catch (const exception& e) {
cerr << "Unable to open file " << fileName << endl;
return 1;
}
for (const auto& element : myInts) {
cout << element << " ";
}
cout << endl;
return 0;
}
You can throw an exception of any type. The preceding example throws an object of type std::exception
, but exceptions do not need to be objects. You could throw a simple int
like this:
vector<int> readIntegerFile(string_view fileName)
{
ifstream inputStream(fileName.data());
if (inputStream.fail()) {
// We failed to open the file: throw an exception
throw 5;
}
// Omitted for brevity
}
You would then need to change the catch
statement as well:
try {
myInts = readIntegerFile(fileName);
} catch (int e) {
cerr << "Unable to open file " << fileName << " (" << e << ")" << endl;
return 1;
}
Alternatively, you could throw a const char*
C-style string. This technique is sometimes useful because the string can contain information about the exception.
vector<int> readIntegerFile(string_view fileName)
{
ifstream inputStream(fileName.data());
if (inputStream.fail()) {
// We failed to open the file: throw an exception
throw "Unable to open file";
}
// Omitted for brevity
}
When you catch the const char*
exception, you can print the result:
try {
myInts = readIntegerFile(fileName);
} catch (const char* e) {
cerr << e << endl;
return 1;
}
Despite the previous examples, you should generally throw objects as exceptions for two reasons:
The C++ Standard Library defines a number of predefined exception classes, and you can write your own exception classes, as you’ll learn later in this chapter.
In the preceding example in which readIntegerFile()
throws an object of type exception
, the catch
line looks like this:
} catch (const exception& e) {
However, there is no requirement to catch objects by const
reference. You could catch the object by value like this:
} catch (exception e) {
Alternatively, you could catch the object by non-const
reference:
} catch (exception& e) {
Also, as you saw in the const char*
example, you can catch pointers to exceptions, as long as pointers to exceptions are thrown.
Failure to open the file is not the only problem readIntegerFile()
could encounter. Reading the data from the file can cause an error if it is formatted incorrectly. Here is an implementation of readIntegerFile()
that throws an exception if it cannot either open the file or read the data correctly. This time, it uses a runtime_error
, derived from exception
, and which allows you to specify a descriptive string in its constructor. The runtime_error
exception class is defined in <stdexcept>
.
vector<int> readIntegerFile(string_view fileName)
{
ifstream inputStream(fileName.data());
if (inputStream.fail()) {
// We failed to open the file: throw an exception
throw runtime_error("Unable to open the file.");
}
// Read the integers one-by-one and add them to a vector
vector<int> integers;
int temp;
while (inputStream >> temp) {
integers.push_back(temp);
}
if (!inputStream.eof()) {
// We did not reach the end-of-file.
// This means that some error occurred while reading the file.
// Throw an exception.
throw runtime_error("Error reading the file.");
}
return integers;
}
Your code in main()
does not need to change because it already catches an exception of type exception
, from which runtime_error
derives. However, that exception could now be thrown in two different situations.
try {
myInts = readIntegerFile(fileName);
} catch (const exception& e) {
cerr << e.what() << endl;
return 1;
}
Alternatively, you could throw two different types of exceptions from readIntegerFile()
. Here is an implementation of readIntegerFile()
that throws an exception object of class invalid_argument
if the file cannot be opened, and an object of class runtime_error
if the integers cannot be read. Both invalid_argument
and runtime_error
are classes defined in the header file <stdexcept>
as part of the C++ Standard Library.
vector<int> readIntegerFile(string_view fileName)
{
ifstream inputStream(fileName.data());
if (inputStream.fail()) {
// We failed to open the file: throw an exception
throw invalid_argument("Unable to open the file.");
}
// Read the integers one-by-one and add them to a vector
vector<int> integers;
int temp;
while (inputStream >> temp) {
integers.push_back(temp);
}
if (!inputStream.eof()) {
// We did not reach the end-of-file.
// This means that some error occurred while reading the file.
// Throw an exception.
throw runtime_error("Error reading the file.");
}
return integers;
}
There are no public default constructors for invalid_argument
and runtime_error
, only string
constructors.
Now main()
can catch both invalid_argument
and runtime_error
with two catch
statements:
try {
myInts = readIntegerFile(fileName);
} catch (const invalid_argument& e) {
cerr << e.what() << endl;
return 1;
} catch (const runtime_error& e) {
cerr << e.what() << endl;
return 2;
}
If an exception is thrown inside the try
block, the compiler matches the type of the exception to the proper catch
handler. So, if readIntegerFile()
is unable to open the file and throws an invalid_argument
object, it is caught by the first catch
statement. If readIntegerFile()
is unable to read the file properly and throws a runtime_error
, then the second catch
statement catches the exception.
The const
-ness specified in the type of the exception you want to catch makes no difference for matching purposes. That is, this line matches any exception of type runtime_error
:
} catch (const runtime_error& e) {
The following line also matches any exception of type runtime_error
:
} catch (runtime_error& e) {
You can write a catch
line that matches any possible exception with the special syntax shown in the following example:
try {
myInts = readIntegerFile(fileName);
} catch (...) {
cerr << "Error reading or opening file " << fileName << endl;
return 1;
}
The three dots are not a typo. They are a wildcard that matches any exception type. When you are calling poorly documented code, this technique can be useful to ensure that you catch all possible exceptions. However, in situations where you have complete information about the set of thrown exceptions, this technique is not recommended because it handles every exception type identically. It’s better to match exception types explicitly and take appropriate, targeted actions.
A possible use of a catch
block matching any exception is as a default catch
handler. When an exception is thrown, a catch
handler is looked up in the order that it appears in the code. The following example shows how you can write catch
handlers that explicitly handle invalid_argument
and runtime_error
exceptions, and how to include a default catch
handler for all other exceptions:
try {
// Code that can throw exceptions
} catch (const invalid_argument& e) {
// Handle invalid_argument exception
} catch (const runtime_error& e) {
// Handle runtime_error exception
} catch (...) {
// Handle all other exceptions
}
If your program throws an exception that is not caught anywhere, the program terminates. Basically, there is a try/catch
construct around the call to your main()
function, which catches all unhandled exceptions, similar to the following:
try {
main(argc, argv);
} catch (...) {
// issue error message and terminate program
}
However, this behavior is not usually what you want. The point of exceptions is to give your program a chance to handle and correct undesirable or unexpected situations.
It is also possible to change the behavior of your program if there is an uncaught exception. When the program encounters an uncaught exception, it calls the built-in terminate()
function, which calls abort()
from <cstdlib>
to kill the program. You can set your own terminate_handler
by calling set_terminate()
with a pointer to a callback function that takes no arguments and returns no value. terminate()
, set_terminate()
, and terminate_handler
are all declared in the <exception>
header. The following code shows a high-level overview of how it works:
try {
main(argc, argv);
} catch (...) {
if (terminate_handler != nullptr) {
terminate_handler();
} else {
terminate();
}
}
// normal termination code
Before you get too excited about this feature, you should know that your callback function must still terminate the program. It can’t just ignore the error. However, you can use it to print a helpful error message before exiting. Here is an example of a main()
function that doesn’t catch the exceptions thrown by readIntegerFile()
. Instead, it sets the terminate_handler
to a custom callback. This callback prints an error message and terminates the process by calling exit()
. The exit()
function accepts an integer which is returned back to the operating system, and which can be used to determine how a process exited.
void myTerminate()
{
cout << "Uncaught exception!" << endl;
exit(1);
}
int main()
{
set_terminate(myTerminate);
const string fileName = "IntegerFile.txt";
vector<int> myInts = readIntegerFile(fileName);
for (const auto& element : myInts) {
cout << element << " ";
}
cout << endl;
return 0;
}
Although not shown in this example, set_terminate()
returns the old terminate_handler
when it sets the new one. The terminate_handler
applies program-wide, so it’s considered good style to reset the old terminate_handler
when you have completed the code that needed the new terminate_handler
. In this case, the entire program needs the new terminate_handler
, so there’s no point in resetting it.
While it’s important to know about set_terminate()
, it’s not a very effective exception-handling approach. It’s recommended to try to catch and handle each exception individually in order to provide more precise error handling.
A function is allowed to throw any exception it likes. However, it is possible to mark a function with the noexcept
keyword to state that it will not throw any exceptions. For example, here is the readIntegerFile()
function from earlier, but this time marked as noexcept
, so it is not allowed to throw any exceptions:
vector<int> readIntegerFile(string_view fileName) noexcept;
When a function marked as noexcept
throws an exception anyway, C++ calls terminate()
to terminate the application.
When you override a virtual
method in a derived class, you are allowed to mark the overridden method as noexcept
, even if the version in the base class is not noexcept
. The opposite is not allowed.
Older versions of C++ allowed you to specify the exceptions a function or method intended to throw. This specification was called the throw list or the exception specification.
Because C++17 has officially removed support for exception specifications, this book does not use them, and does not explain them in detail. Even while exception specifications were still supported, they were used rarely. Still, this section briefly mentions them, so you will at least get an idea of what the syntax looked like in the unlikely event you encounter them in legacy code. Here is a simple example of the syntax, the readIntegerFile()
function from earlier, with an exception specification:
vector<int> readIntegerFile(string_view fileName)
throw(invalid_argument, runtime_error)
{
// Remainder of the function is the same as before
}
If a function threw an exception that was not in its exception specification, the C++ runtime called std::unexpected()
which by default called std::terminate()
to terminate the application.
As described earlier, you can actually throw any type of exception. However, classes are the most useful types of exceptions. In fact, exception classes are usually written in a hierarchy, so that you can employ polymorphism when you catch the exceptions.
You’ve already seen several exceptions from the C++ standard exception hierarchy: exception
, runtime_error
, and invalid_argument
. Figure 14-3 shows the full hierarchy. For completeness, all standard exceptions are shown, including those thrown by parts of the Standard Library, which are discussed in later chapters.
All of the exceptions thrown by the C++ Standard Library are objects of classes in this hierarchy. Each class in the hierarchy supports a what()
method that returns a const
char*
string describing the exception. You can use this string in an error message.
Most of the exception classes (a notable exception is the base exception
class) require you to set in the constructor the string that is returned by what()
. That’s why you have to specify a string in the constructors for runtime_error
and invalid_argument
. This has already been done in examples throughout this chapter. Here is another version of readIntegerFile()
that includes the filename in the error message. Note also the use of the standard user-defined literal “s
”, introduced in Chapter 2, to interpret a string literal as an std::string
.
vector<int> readIntegerFile(string_view fileName)
{
ifstream inputStream(fileName.data());
if (inputStream.fail()) {
// We failed to open the file: throw an exception
const string error = "Unable to open file "s + fileName.data();
throw invalid_argument(error);
}
// Read the integers one-by-one and add them to a vector
vector<int> integers;
int temp;
while (inputStream >> temp) {
integers.push_back(temp);
}
if (!inputStream.eof()) {
// We did not reach the end-of-file.
// This means that some error occurred while reading the file.
// Throw an exception.
const string error = "Unable to read file "s + fileName.data();
throw runtime_error(error);
}
return integers;
}
int main()
{
// Code omitted
try {
myInts = readIntegerFile(fileName);
} catch (const invalid_argument& e) {
cerr << e.what() << endl;
return 1;
} catch (const runtime_error& e) {
cerr << e.what() << endl;
return 2;
}
// Code omitted
}
A feature of exception hierarchies is that you can catch exceptions polymorphically. For example, if you look at the two catch
statements in main()
following the call to readIntegerFile()
, you can see that they are identical except for the exception class that they handle. Conveniently, invalid_argument
and runtime_error
are both derived classes of exception
, so you can replace the two catch
statements with a single catch
statement for class exception
:
try {
myInts = readIntegerFile(fileName);
} catch (const exception& e) {
cerr << e.what() << endl;
return 2;
}
The catch
statement for an exception
reference matches any derived classes of exception
, including both invalid_argument
and runtime_error
. Note that the higher in the exception hierarchy you catch exceptions, the less specific your error handling can be. You should generally catch exceptions at as specific a level as possible.
When more than one catch
clause is used, the catch
clauses are matched in syntactic order as they appear in your code; the first one that matches, wins. If one catch
is more inclusive than a later one, it will match first, and the more restrictive one, which comes later, will not be executed at all. Therefore, you should place your catch
clauses from most restrictive to least restrictive in order. For example, suppose that you want to catch invalid_argument
from readIntegerFile()
explicitly, but you also want to leave the generic exception
match for any other exceptions. The correct way to do so is like this:
try {
myInts = readIntegerFile(fileName);
} catch (const invalid_argument& e) { // List the derived class first.
// Take some special action for invalid filenames.
} catch (const exception& e) { // Now list exception
cerr << e.what() << endl;
return 1;
}
The first catch
statement catches invalid_argument
exceptions, and the second catches any other exceptions of type exception
. However, if you reverse the order of the catch
statements, you don’t get the same result:
try {
myInts = readIntegerFile(fileName);
} catch (const exception& e) { // BUG: catching base class first!
cerr << e.what() << endl;
return 1;
} catch (const invalid_argument& e) {
// Take some special action for invalid filenames.
}
With this order, any exception of a class that derives from exception
is caught by the first catch
statement; the second will never be reached. Some compilers issue a warning in this case, but you shouldn’t count on it.
There are two advantages to writing your own exception classes.
runtime_error
, you can create classes with names that are more meaningful for the particular errors in your program.It’s recommended that all the exception classes that you write inherit directly or indirectly from the standard exception
class. If everyone on your project follows that rule, you know that every exception in the program will be derived from exception
(assuming that you aren’t using third-party libraries that break this rule). This guideline makes exception handling via polymorphism significantly easier.
For example, invalid_argument
and runtime_error
don’t do a very good job of capturing the file opening and reading errors in readIntegerFile()
. You can define your own error hierarchy for file errors, starting with a generic FileError
class:
class FileError : public exception
{
public:
FileError(string_view fileName) : mFileName(fileName) {}
virtual const char* what() const noexcept override {
return mMessage.c_str();
}
string_view getFileName() const noexcept { return mFileName; }
protected:
void setMessage(string_view message) { mMessage = message; }
private:
string mFileName;
string mMessage;
};
As a good programming citizen, you should make FileError
a part of the standard exception hierarchy. It seems appropriate to integrate it as a child of exception
. When you derive from exception
, you can override the what()
method, which has the prototype shown and which must return a const
char*
string that is valid until the object is destroyed. In the case of FileError
, this string comes from the mMessage
data member. Derived classes of FileError
can set the message using the protected
setMessage()
method. The generic FileError
class also contains a filename, and a public accessor for that filename.
The first exceptional situation in readIntegerFile()
occurs if the file cannot be opened. Thus, you might want to write a FileOpenError
exception derived from FileError
:
class FileOpenError : public FileError
{
public:
FileOpenError(string_view fileName) : FileError(fileName)
{
setMessage("Unable to open "s + fileName.data());
}
};
The FileOpenError
exception changes the mMessage
string to represent the file-opening error.
The second exceptional situation in readIntegerFile()
occurs if the file cannot be read properly. It might be useful for this exception to contain the line number of the error in the file, as well as the filename in the error message string returned from what()
. Here is a FileReadError
exception derived from FileError
:
class FileReadError : public FileError
{
public:
FileReadError(string_view fileName, size_t lineNumber)
: FileError(fileName), mLineNumber(lineNumber)
{
ostringstream ostr;
ostr << "Error reading " << fileName << " at line "
<< lineNumber;
setMessage(ostr.str());
}
size_t getLineNumber() const noexcept { return mLineNumber; }
private:
size_t mLineNumber;
};
Of course, in order to set the line number properly, you need to modify your readIntegerFile()
function to track the number of lines read instead of just reading integers directly. Here is a new readIntegerFile()
function that uses the new exceptions:
vector<int> readIntegerFile(string_view fileName)
{
ifstream inputStream(fileName.data());
if (inputStream.fail()) {
// We failed to open the file: throw an exception
throw FileOpenError(fileName);
}
vector<int> integers;
size_t lineNumber = 0;
while (!inputStream.eof()) {
// Read one line from the file
string line;
getline(inputStream, line);
++lineNumber;
// Create a string stream out of the line
istringstream lineStream(line);
// Read the integers one-by-one and add them to a vector
int temp;
while (lineStream >> temp) {
integers.push_back(temp);
}
if (!lineStream.eof()) {
// We did not reach the end of the string stream.
// This means that some error occurred while reading this line.
// Throw an exception.
throw FileReadError(fileName, lineNumber);
}
}
return integers;
}
Now, code that calls readIntegerFile()
can use polymorphism to catch exceptions of type FileError
like this:
try {
myInts = readIntegerFile(fileName);
} catch (const FileError& e) {
cerr << e.what() << endl;
return 1;
}
There is one trick to writing classes whose objects will be used as exceptions. When a piece of code throws an exception, the object or value thrown is moved or copied, using either the move constructor or copy constructor. Thus, if you write a class whose objects will be thrown as exceptions, you must make sure those objects are copyable and/or moveable. This means that if you have dynamically allocated memory, your class must have a destructor, but also a copy constructor and copy assignment operator, and/or a move constructor and move assignment operator, see Chapter 9.
It is possible for exceptions to be copied more than once, but only if you catch the exception by value instead of by reference.
It could happen that during handling of a first exception, a second exceptional situation is triggered which requires a second exception to be thrown. Unfortunately, when you throw the second exception, all information about the first exception that you are currently trying to handle will be lost. The solution provided by C++ for this problem is called nested exceptions, which allow you to nest a caught exception in the context of a new exception. This can also be useful if you call a function in a third-party library that throws an exception of a certain type, A
, but you only want exceptions of another type, B
, in your code. In such a case, you catch all exceptions from the library, and nest them in an exception of type B
.
You use std::throw_with_nested()
to throw an exception with another exception nested inside it. A catch handler for the second exception can use a dynamic_cast()
to get access to the nested_exception
representing the first exception. The following example demonstrates the use of nested exceptions. This example defines a MyException
class, which derives from exception
and accepts a string in its constructor.
class MyException : public std::exception
{
public:
MyException(string_view message) : mMessage(message) {}
virtual const char* what() const noexcept override {
return mMessage.c_str();
}
private:
string mMessage;
};
When you are handling a first exception and you need to throw a second exception with the first one nested inside it, you need to use the std::throw_with_nested()
function. The following doSomething()
function throws a runtime_error
that is immediately caught in the catch handler. The catch handler writes a message and then uses the throw_with_nested()
function to throw a second exception that has the first one nested inside it. Note that nesting the exception happens automatically. The predefined __func__
variable is discussed in Chapter 1.
void doSomething()
{
try {
throw runtime_error("Throwing a runtime_error exception");
} catch (const runtime_error& e) {
cout << __func__ << " caught a runtime_error" << endl;
cout << __func__ << " throwing MyException" << endl;
throw_with_nested(
MyException("MyException with nested runtime_error"));
}
}
The following main()
function demonstrates how to handle the exception with a nested exception. The code calls the doSomething()
function and has one catch handler for exceptions of type MyException
. When it catches such an exception, it writes a message, and then uses a dynamic_cast()
to get access to the nested exception. If there is no nested exception inside, the result will be a null pointer. If there is a nested exception inside, the rethrow_nested()
method on the nested_exception
is called. This will cause the nested exception to be rethrown, which you can then catch in another try/catch
block.
int main()
{
try {
doSomething();
} catch (const MyException& e) {
cout << __func__ << " caught MyException: " << e.what() << endl;
const auto* pNested = dynamic_cast<const nested_exception*>(&e);
if (pNested) {
try {
pNested->rethrow_nested();
} catch (const runtime_error& e) {
// Handle nested exception
cout << " Nested exception: " << e.what() << endl;
}
}
}
return 0;
}
The output should be as follows:
doSomething caught a runtime_error
doSomething throwing MyException
main caught MyException: MyException with nested runtime_error
Nested exception: Throwing a runtime_error exception
The preceding main()
function uses a dynamic_cast()
to check for the nested exception. Because you often have to perform this dynamic_cast()
if you want to check for a nested exception, the standard provides a small helper function called std::rethrow_if_nested()
that does it for you. This helper function can be used as follows:
int main()
{
try {
doSomething();
} catch (const MyException& e) {
cout << __func__ << " caught MyException: " << e.what() << endl;
try {
rethrow_if_nested(e);
} catch (const runtime_error& e) {
// Handle nested exception
cout << " Nested exception: " << e.what() << endl;
}
}
return 0;
}
The throw
keyword can also be used to rethrow the current exception, as in the following example:
void g() { throw invalid_argument("Some exception"); }
void f()
{
try {
g();
} catch (const invalid_argument& e) {
cout << "caught in f: " << e.what() << endl;
throw; // rethrow
}
}
int main()
{
try {
f();
} catch (const invalid_argument& e) {
cout << "caught in main: " << e.what() << endl;
}
return 0;
}
This example produces the following output:
caught in f: Some exception
caught in main: Some exception
You might think you could also rethrow an exception using something like a “throw e
;” statement; however, that’s wrong, because it can cause slicing of your exception object. For example, suppose f()
is modified to catch std::exception
s, and main()
is modified to catch both exception
and invalid_argument
exceptions:
void g() { throw invalid_argument("Some exception"); }
void f()
{
try {
g();
} catch (const exception& e) {
cout << "caught in f: " << e.what() << endl;
throw; // rethrow
}
}
int main()
{
try {
f();
} catch (const invalid_argument& e) {
cout << "invalid_argument caught in main: " << e.what() << endl;
} catch (const exception& e) {
cout << "exception caught in main: " << e.what() << endl;
}
return 0;
}
Remember that invalid_argument
derives from exception
. The output of this code is as you would expect:
caught in f: Some exception
invalid_argument caught in main: Some exception
However, try replacing the “throw;
” line in f()
with:
throw e;
The output then is as follows:
caught in f: Some exception
exception caught in main: Some exception
Now main()
seems to be catching an exception
object, instead of an invalid_argument
object. That’s because the “throw e;
” statement causes slicing, reducing the invalid_argument
to an exception
.
When a piece of code throws an exception, it searches for a catch handler on the stack. This catch handler could be zero or more function calls up the stack of execution. When one is found, the stack is stripped back to the stack level that defines the catch handler by unwinding all intermediate stack frames. Stack unwinding means that the destructors for all locally scoped names are called, and all code remaining in each function past the current point of execution is skipped.
However, in stack unwinding, pointer variables are not freed, and other cleanup is not performed. This behavior can present problems, as the following code demonstrates:
void funcOne();
void funcTwo();
int main()
{
try {
funcOne();
} catch (const exception& e) {
cerr << "Exception caught!" << endl;
return 1;
}
return 0;
}
void funcOne()
{
string str1;
string* str2 = new string();
funcTwo();
delete str2;
}
void funcTwo()
{
ifstream fileStream;
fileStream.open("filename");
throw exception();
fileStream.close();
}
When funcTwo()
throws an exception, the closest exception handler is in main()
. Control then jumps immediately from this line in funcTwo()
,
throw exception();
to this line in main()
:
cerr << "Exception caught!" << endl;
In funcTwo()
, control remains at the line that threw the exception, so this subsequent line never gets a chance to run:
fileStream.close();
However, luckily for you, the ifstream
destructor is called because fileStream
is a local variable on the stack. The ifstream
destructor closes the file for you, so there is no resource leak here. If you had dynamically allocated fileStream
, it would not be destroyed, and the file would not be closed.
In funcOne()
, control is at the call to funcTwo()
, so this subsequent line never gets a chance to run:
delete str2;
In this case, there really is a memory leak. Stack unwinding does not automatically call delete
on str2
for you. However, str1
is destroyed properly because it is a local variable on the stack. Stack unwinding destroys all local variables correctly.
This is one reason why you should never mix older C models of allocation (even if you are calling new
so it looks like C++) with modern programming methodologies like exceptions. In C++, this situation should be handled by using stack-based allocations, or if that is not possible, by one of the techniques discussed in the following two sections.
If stack-based allocation is not possible, then use smart pointers. They allow you to write code that automatically prevents memory or resource leaks with exception handling. Whenever a smart pointer object is destroyed, it frees the underlying resource. Here is an example of the previous funcOne()
function but using the unique_ptr
smart pointer, defined in <memory>
, and introduced in Chapter 1:
void funcOne()
{
string str1;
auto str2 = make_unique<string>("hello");
funcTwo();
}
The str2
pointer will automatically be deleted when you return from funcOne()
or when an exception is thrown.
Of course, you should only allocate something dynamically if you have a good reason to do so. For example, in the previous funcOne()
function, there is no good reason to make str2
a dynamically allocated string. It should just be a stack-based string
variable. It’s merely shown here as a compact example of the consequences of throwing exceptions.
Another technique for avoiding memory and resource leaks is for each function to catch any possible exceptions, perform necessary cleanup work, and rethrow the exception for the function higher up the stack to handle. Here is a revised funcOne()
with this technique:
void funcOne()
{
string str1;
string* str2 = new string();
try {
funcTwo();
} catch (...) {
delete str2;
throw; // Rethrow the exception.
}
delete str2;
}
This function wraps the call to funcTwo()
with an exception handler that performs the cleanup (calls delete
on str2
) and then rethrows the exception. The keyword throw
by itself rethrows whatever exception was caught most recently. Note that the catch statement uses the ...
syntax to catch any exception.
This method works fine, but is messy. In particular, note that there are now two identical lines that call delete
on str2
: one to handle the exception and one if the function exits normally.
Whether or not you use exceptions in your programs is up to you and your colleagues. However, you are strongly encouraged to formalize an error-handling plan for your programs, regardless of your use of exceptions. If you use exceptions, it is generally easier to come up with a unified error-handling scheme, but it is not impossible without exceptions. The most important aspect of a good plan is uniformity of error handling throughout all the modules of the program. Make sure that every programmer on the project understands and follows the error-handling rules.
This section discusses the most common error-handling issues in the context of exceptions, but the issues are also relevant to programs that do not use exceptions.
Despite the fact that all the examples so far in this book have ignored the possibility, memory allocation can fail. On current 64-bit platforms, this will almost never happen, but on mobile or legacy systems, memory allocation can fail. On such systems, you must account for memory allocation failures. C++ provides several different ways to handle memory errors.
The default behaviors of new
and new[]
are to throw an exception of type bad_alloc
, defined in the <new>
header file, if they cannot allocate memory. Your code could catch these exceptions and handle them appropriately.
It’s not realistic to wrap all your calls to new
and new[]
with a try/catch
, but at least you should do so when you are trying to allocate a big block of memory. The following example demonstrates how to catch memory allocation exceptions:
int* ptr = nullptr;
size_t integerCount = numeric_limits<size_t>::max();
try {
ptr = new int[integerCount];
} catch (const bad_alloc& e) {
cerr << __FILE__ << "(" << __LINE__
<< "): Unable to allocate memory: " << e.what() << endl;
// Handle memory allocation failure.
return;
}
// Proceed with function that assumes memory has been allocated.
Note that this code uses the predefined preprocessor symbols __FILE__
and __LINE__
, which are replaced with the name of the file and the current line number. This makes debugging easier.
You could, of course, bulk handle many possible new failures with a single try
/catch
block at a higher point in the program, if that works for your program.
Another point to consider is that logging an error might try to allocate memory. If new
fails, there might not be enough memory left even to log the error message.
If you don’t like exceptions, you can revert to the old C model in which memory allocation routines return a null pointer if they cannot allocate memory. C++ provides nothrow versions of new
and new[]
, which return nullptr
instead of throwing an exception if they fail to allocate memory. This is done by using the syntax new(nothrow)
instead of new
, as shown in the following example:
int* ptr = new(nothrow) int[integerCount];
if (ptr == nullptr) {
cerr << __FILE__ << "(" << __LINE__
<< "): Unable to allocate memory!" << endl;
// Handle memory allocation failure.
return;
}
// Proceed with function that assumes memory has been allocated.
The syntax is a little strange: you really do write “nothrow” as if it’s an argument to new
(which it is).
C++ allows you to specify a new handler callback function. By default, there is no new handler, so new
and new[]
just throw bad_alloc
exceptions. However, if there is a new handler, the memory allocation routine calls the new handler upon memory allocation failure instead of throwing an exception. If the new handler returns, the memory allocation routines attempt to allocate memory again, calling the new handler again if they fail. This cycle could become an infinite loop unless your new handler changes the situation with one of three alternatives. Practically speaking, some of the options are better than others. Here is the list with commentary:
bad_alloc
exception, or an exception derived from bad_alloc
. For example:
document_recovery_alloc
exception, inheriting from bad_alloc
. This exception can be caught somewhere in your application to trigger the document save operation and restart of the application.please_terminate_me
exception, deriving from bad_alloc
. In your top-level function—for example, main()
—you catch this exception and handle it by returning from the top-level function. It’s recommended to terminate a program by returning from the top-level function, instead of by calling exit()
.If you don’t do one of these three things in your new handler, any memory allocation failure will cause an infinite loop.
If there are some memory allocations that can fail but you don’t want the new handler to be called, you can simply set the new handler back to its default of nullptr
temporarily before calling new
in those cases.
You set the new handler with a call to set_new_handler()
, declared in the <new>
header file. Here is an example of a new handler that logs an error message and throws an exception:
class please_terminate_me : public bad_alloc { };
void myNewHandler()
{
cerr << "Unable to allocate memory." << endl;
throw please_terminate_me();
}
The new handler must take no arguments and return no value. This new handler throws a please_terminate_me
exception, as suggested in the second bullet in the preceding list.
You can set this new handler like this:
int main()
{
try {
// Set the new new_handler and save the old one.
new_handler oldHandler = set_new_handler(myNewHandler);
// Generate allocation error
size_t numInts = numeric_limits<size_t>::max();
int* ptr = new int[numInts];
// Reset the old new_handler
set_new_handler(oldHandler);
} catch (const please_terminate_me&) {
cerr << __FILE__ << "(" << __LINE__
<< "): Terminating program." << endl;
return 1;
}
return 0;
}
Note that new_handler
is a typedef
for the type of function pointer that set_new_handler()
takes.
Before C++ programmers discover exceptions, they are often stymied by error handling and constructors. What if a constructor fails to construct the object properly? Constructors don’t have a return value, so the standard pre-exception error-handling mechanism doesn’t work. Without exceptions, the best you can do is to set a flag in the object specifying that it is not constructed properly. You can provide a method, with a name like checkConstructionStatus()
, which returns the value of that flag, and hope that clients remember to call this method on the object after constructing it.
Exceptions provide a much better solution. You can throw an exception from a constructor, even though you can’t return a value. With exceptions, you can easily tell clients whether or not construction of an object succeeded. However, there is one major problem: if an exception leaves a constructor, the destructor for that object will never be called! Thus, you must be careful to clean up any resources and free any allocated memory in constructors before allowing exceptions to leave the constructor. This problem is the same as in any other function, but it is subtler in constructors because you’re accustomed to letting the destructors take care of the memory deallocation and resource freeing.
This section describes a Matrix
class template as an example in which the constructor correctly handles exceptions. Note that this example is using a raw pointer called mMatrix
to demonstrate the problems. In production-quality code, you should avoid using raw pointers, for example, by using a Standard Library container! The definition of the Matrix
class template looks like this:
template <typename T>
class Matrix
{
public:
Matrix(size_t width, size_t height);
virtual ~Matrix();
private:
void cleanup();
size_t mWidth = 0;
size_t mHeight = 0;
T** mMatrix = nullptr;
};
The implementation of the Matrix
class is as follows. Note that the first call to new
is not protected with a try/catch
block. It doesn’t matter if the first new
throws an exception because the constructor hasn’t allocated anything else yet that needs freeing. If any of the subsequent new
calls throws an exception, though, the constructor must clean up all of the memory already allocated. However, it doesn’t know what exceptions the T
constructors themselves might throw, so it catches any exceptions via ...
and then nests the caught exception inside a bad_alloc
exception. Note that the array allocated with the first call to new
is zero-initialized using the {}
syntax, that is, each element will be nullptr
. This makes the cleanup()
method easier, because it is allowed to call delete
on a nullptr
.
template <typename T>
Matrix<T>::Matrix(size_t width, size_t height)
{
mMatrix = new T*[width] {}; // Array is zero-initialized!
// Don't initialize the mWidth and mHeight members in the ctor-
// initializer. These should only be initialized when the above
// mMatrix allocation succeeds!
mWidth = width;
mHeight = height;
try {
for (size_t i = 0; i < width; ++i) {
mMatrix[i] = new T[height];
}
} catch (...) {
std::cerr << "Exception caught in constructor, cleaning up..."
<< std::endl;
cleanup();
// Nest any caught exception inside a bad_alloc exception.
std::throw_with_nested(std::bad_alloc());
}
}
template <typename T>
Matrix<T>::~Matrix()
{
cleanup();
}
template <typename T>
void Matrix<T>::cleanup()
{
for (size_t i = 0; i < mWidth; ++i)
delete[] mMatrix[i];
delete[] mMatrix;
mMatrix = nullptr;
mWidth = mHeight = 0;
}
The Matrix
class template can be tested as follows:
class Element
{
// Kept to a bare minimum, but in practice, this Element class
// could throw exceptions in its constructor.
private:
int mValue;
};
int main()
{
Matrix<Element> m(10, 10);
return 0;
}
You might be wondering what happens when you add inheritance into the mix. Base class constructors run before derived class constructors. If a derived class constructor throws an exception, C++ will execute the destructor of the fully constructed base class.
The exception mechanism, as discussed up to now in this chapter, is perfect for handling exceptions within functions. But how should you handle exceptions thrown from inside a ctor-initializer of a constructor? This section explains a feature called function-try-blocks, which are capable of catching those exceptions. Function-try-blocks work for normal functions as well as for constructors. This section focuses on the use with constructors. Most C++ programmers, even experienced C++ programmers, don’t know of the existence of this feature, even though it was introduced a long time ago.
The following piece of pseudo-code shows the basic syntax for a function-try-block for a constructor:
MyClass::MyClass()
try
: <ctor-initializer>
{
/* ... constructor body ... */
}
catch (const exception& e)
{
/* ... */
}
As you can see, the try
keyword should be right before the start of the ctor-initializer. The catch
statements should be after the closing brace for the constructor, actually putting them outside the constructor body. There are a number of restrictions and guidelines that you should keep in mind when using function-try-blocks with constructors:
catch
statements catch any exception thrown either directly or indirectly by the ctor-initializer or by the constructor body.catch
statements have to rethrow the current exception or throw a new exception. If a catch
statement doesn’t do this, the runtime automatically rethrows the current exception.catch
statements can access arguments passed to the constructor.catch
statement catches an exception in a function-try-block, all fully constructed base classes and members of the object are destroyed before execution of the catch
statement starts.catch
statements you should not access member variables that are objects because these are destroyed prior to executing the catch
statements (see the previous bullet). However, if your object contains non-class data members—for example, raw pointers—you can access them if they have been initialized before the exception was thrown. If you have such naked resources, you have to take care of them by freeing them in the catch
statements, as the following example demonstrates.catch
statements in a function-try-block cannot use the return
keyword to return a value from the function enclosed by it. This is not relevant for constructors because they do not return anything.Based on this list of limitations, function-try-blocks for constructors are useful only in a limited number of situations:
The following example demonstrates how to use function-try-blocks. The code defines a class called SubObject
. It has only one constructor, which throws an exception of type runtime_error
.
class SubObject
{
public:
SubObject(int i);
};
SubObject::SubObject(int i)
{
throw std::runtime_error("Exception by SubObject ctor");
}
The MyClass
class has a member variable of type int*
and another one of type SubObject
:
class MyClass
{
public:
MyClass();
private:
int* mData = nullptr;
SubObject mSubObject;
};
The SubObject
class does not have a default constructor. This means that you need to initialize mSubObject
in the MyClass
ctor-initializer. The constructor of MyClass
uses a function-try-block to catch exceptions thrown in its ctor-initializer:
MyClass::MyClass()
try
: mData(new int[42]{ 1, 2, 3 }), mSubObject(42)
{
/* ... constructor body ... */
}
catch (const std::exception& e)
{
// Cleanup memory.
delete[] mData;
mData = nullptr;
cout << "function-try-block caught: '" << e.what() << "'" << endl;
}
Remember that catch
statements in a function-try-block for a constructor have to either rethrow the current exception, or throw a new exception. The preceding catch
statement does not throw anything, so the C++ runtime automatically rethrows the current exception. Following is a simple function that uses the preceding class:
int main()
{
try {
MyClass m;
} catch (const std::exception& e) {
cout << "main() caught: '" << e.what() << "'" << endl;
}
return 0;
}
The output of the preceding example is as follows:
function-try-block caught: 'Exception by SubObject ctor'
main() caught: 'Exception by SubObject ctor'
Note that the code in the example can be dangerous. Depending on the order of initialization, it could be that mData
contains garbage when entering the catch
statement. Deleting such a garbage pointer causes undefined behavior. The solution in this example’s case is to use a smart pointer for the mData
member, for example std::unique_ptr
, and to remove the function-try-block. Therefore:
Function-try-blocks are not limited to constructors. They can be used with ordinary functions as well. However, for normal functions, there is no useful reason to use function-try-blocks because they can just as easily be converted to a simple try/catch
block inside the function body. One notable difference when using a function-try-block on a normal function compared to a constructor is that rethrowing the current exception or throwing a new exception in the catch
statements is not required and the C++ runtime will not automatically rethrow the exception.
You should handle all error conditions that arise in destructors in the destructors themselves. You should not let any exceptions be thrown from destructors, for a couple of reasons:
noexcept
, unless they are marked with noexcept(false)
2 or the class has any subobjects whose destructor is noexcept(false)
. If you throw an exception from a noexcept
destructor, the C++ runtime calls std::terminate()
to terminate the application.std::terminate()
to terminate the application. For the brave and curious, C++ does provide the ability to determine, in a destructor, whether you are executing as a result of a normal function exit or delete call, or because of stack unwinding. The function uncaught_exceptions()
, declared in the <exception>
header file, returns the number of uncaught exceptions, that is, exceptions that have been thrown but that have not reached a matching catch
yet. If the result of uncaught_exceptions()
is greater than zero, then you are in the middle of stack unwinding. However, correct use of this function is complicated, messy, and should be avoided. Note that before C++17, the function was called uncaught_exception()
(singular), and returned a bool
that was true
if you were in the middle of stack unwinding.delete
, which calls the destructor. If you throw an exception from the destructor, what is a client supposed to do? It can’t call delete
on the object again, and it shouldn’t call the destructor explicitly. There is no reasonable action the client can take, so there is no reason to burden that code with exception handling.Now that you’ve learned about error handling and exceptions, let’s see it all coming together in a bigger example, a GameBoard
class. This GameBoard
class is based on the GameBoard
class from Chapter 12. The implementation in Chapter 12 using a vector
of vectors
is the recommended implementation because even when an exception is thrown, the code is not leaking any memory due to the use of Standard Library containers. To be able to demonstrate handling memory allocation errors, the following version is adapted to use a raw pointer, GamePiece** mCells
. First, here is the definition of the class without any exceptions:
class GamePiece {};
class GameBoard
{
public:
// general-purpose GameBoard allows user to specify its dimensions
explicit GameBoard(size_t width = kDefaultWidth,
size_t height = kDefaultHeight);
GameBoard(const GameBoard& src); // Copy constructor
virtual ~GameBoard();
GameBoard& operator=(const GameBoard& rhs); // Assignment operator
GamePiece& at(size_t x, size_t y);
const GamePiece& at(size_t x, size_t y) const;
size_t getHeight() const { return mHeight; }
size_t getWidth() const { return mWidth; }
static const size_t kDefaultWidth = 100;
static const size_t kDefaultHeight = 100;
friend void swap(GameBoard& first, GameBoard& second) noexcept;
private:
// Objects dynamically allocate space for the game pieces.
GamePiece** mCells = nullptr;
size_t mWidth = 0, mHeight = 0;
};
Note that you could also add move semantics to this GameBoard
class. That would require adding a move constructor and move assignment operator, which both have to be noexcept
. See Chapter 9 for details on move semantics.
Here are the implementations without any exceptions:
GameBoard::GameBoard(size_t width, size_t height)
: mWidth(width), mHeight(height)
{
mCells = new GamePiece*[mWidth];
for (size_t i = 0; i < mWidth; i++) {
mCells[i] = new GamePiece[mHeight];
}
}
GameBoard::GameBoard(const GameBoard& src)
: GameBoard(src.mWidth, src.mHeight)
{
// The ctor-initializer of this constructor delegates first to the
// non-copy constructor to allocate the proper amount of memory.
// The next step is to copy the data.
for (size_t i = 0; i < mWidth; i++) {
for (size_t j = 0; j < mHeight; j++) {
mCells[i][j] = src.mCells[i][j];
}
}
}
GameBoard::~GameBoard()
{
for (size_t i = 0; i < mWidth; i++) {
delete[] mCells[i];
}
delete[] mCells;
mCells = nullptr;
mWidth = mHeight = 0;
}
void swap(GameBoard& first, GameBoard& second) noexcept
{
using std::swap;
swap(first.mWidth, second.mWidth);
swap(first.mHeight, second.mHeight);
swap(first.mCells, second.mCells);
}
GameBoard& GameBoard::operator=(const GameBoard& rhs)
{
// Check for self-assignment
if (this == &rhs) {
return *this;
}
// Copy-and-swap idiom
GameBoard temp(rhs); // Do all the work in a temporary instance
swap(*this, temp); // Commit the work with only non-throwing operations
return *this;
}
const GamePiece& GameBoard::at(size_t x, size_t y) const
{
return mCells[x][y];
}
GamePiece& GameBoard::at(size_t x, size_t y)
{
return const_cast<GamePiece&>(std::as_const(*this).at(x, y));
}
Now, let’s retrofit the preceding class to include error handling and exceptions. The constructors and operator=
can all throw bad_alloc
because they perform memory allocation directly or indirectly. The destructor, cleanup()
, getHeight()
, getWidth()
, and swap()
throw no exceptions. The verifyCoordinate()
and at()
methods throw out_of_range
if the caller supplies an invalid coordinate. Here is the retrofitted class definition:
class GamePiece {};
class GameBoard
{
public:
explicit GameBoard(size_t width = kDefaultWidth,
size_t height = kDefaultHeight);
GameBoard(const GameBoard& src);
virtual ~GameBoard() noexcept;
GameBoard& operator=(const GameBoard& rhs); // Assignment operator
GamePiece& at(size_t x, size_t y);
const GamePiece& at(size_t x, size_t y) const;
size_t getHeight() const noexcept { return mHeight; }
size_t getWidth() const noexcept { return mWidth; }
static const size_t kDefaultWidth = 100;
static const size_t kDefaultHeight = 100;
friend void swap(GameBoard& first, GameBoard& second) noexcept;
private:
void cleanup() noexcept;
void verifyCoordinate(size_t x, size_t y) const;
GamePiece** mCells = nullptr;
size_t mWidth = 0, mHeight = 0;
};
Here are the implementations with exception handling:
GameBoard::GameBoard(size_t width, size_t height)
{
mCells = new GamePiece*[width] {}; // Array is zero-initialized!
// Don't initialize the mWidth and mHeight members in the ctor-
// initializer. These should only be initialized when the above
// mCells allocation succeeds!
mWidth = width;
mHeight = height;
try {
for (size_t i = 0; i < mWidth; i++) {
mCells[i] = new GamePiece[mHeight];
}
} catch (...) {
cleanup();
// Nest any caught exception inside a bad_alloc exception.
std::throw_with_nested(std::bad_alloc());
}
}
GameBoard::GameBoard(const GameBoard& src)
: GameBoard(src.mWidth, src.mHeight)
{
// The ctor-initializer of this constructor delegates first to the
// non-copy constructor to allocate the proper amount of memory.
// The next step is to copy the data.
for (size_t i = 0; i < mWidth; i++) {
for (size_t j = 0; j < mHeight; j++) {
mCells[i][j] = src.mCells[i][j];
}
}
}
GameBoard::~GameBoard() noexcept
{
cleanup();
}
void GameBoard::cleanup() noexcept
{
for (size_t i = 0; i < mWidth; i++)
delete[] mCells[i];
delete[] mCells;
mCells = nullptr;
mWidth = mHeight = 0;
}
void GameBoard::verifyCoordinate(size_t x, size_t y) const
{
if (x >= mWidth)
throw out_of_range("x-coordinate beyond width");
if (y >= mHeight)
throw out_of_range("y-coordinate beyond height");
}
void swap(GameBoard& first, GameBoard& second) noexcept
{
using std::swap;
swap(first.mWidth, second.mWidth);
swap(first.mHeight, second.mHeight);
swap(first.mCells, second.mCells);
}
GameBoard& GameBoard::operator=(const GameBoard& rhs)
{
// Check for self-assignment
if (this == &rhs) {
return *this;
}
// Copy-and-swap idiom
GameBoard temp(rhs); // Do all the work in a temporary instance
swap(*this, temp); // Commit the work with only non-throwing operations
return *this;
}
const GamePiece& GameBoard::at(size_t x, size_t y) const
{
verifyCoordinate(x, y);
return mCells[x][y];
}
GamePiece& GameBoard::at(size_t x, size_t y)
{
return const_cast<GamePiece&>(std::as_const(*this).at(x, y));
}
This chapter described the issues related to error handling in C++ programs, and emphasized that you must design and code your programs with an error-handling plan. By reading this chapter, you learned the details of C++ exceptions syntax and behavior. The chapter also covered some of the areas in which error handling plays a large role, including I/O streams, memory allocation, constructors, and destructors. Finally, you saw an example of error handling in a GameBoard
class.