8. Exceptions#
As introductory students developing programs for class assignments, validating input and terminating upon invalid input or terminating after detecting runtime errors usually suffices. However, for robust, production-quality code, we need to detect, respond, and recover from a variety of errors. We start to develop robust programs through rigorous input validation, but that validation may not cover (or prevent) the various run-time errors that can occur. For instance, if a user selects a given input file, we only know it is valid after we complete parsing/processing it. A number of issues may arise along the way and it’s nearly impossible to prevent all such problems.
Exceptions are run-time errors that occur outside of the normal functioning of a program. We use exception handling when one part of a program detects a problem it cannot resolve. That part signals (by throwing an exception) that an issue has occurred. Control will then pass to another part of the program that can handle that exception. While typical examples will show the two parts co-located in a try-catch
statement, the detecting error may occur within functions called by the statements in the try
block. The exception basically jumps to the point in the call stack that can handle the handle - this creates a clean separation between error detection and error recovery/handling.
Similar to Python, C++ provides exception handling to deal with unusual conditions during the runtime of a program in a controlled and systematic manner. When an exception occurs, the normal flow of the program is interrupted, and control is transferred to a special code block called a handler. Our goal in these handlers is to return the program to a valid state or to gracefully exit the program if that is impossible.
Python and C++ have similar syntax for exception handling. C++ syntax -
try {
// code that may throw exception(s)
} catch (const ExceptionType1 &ex1) {
// code to handle ExceptionType1
} catch (const ExceptionType2 &ex2) {
// code to handle ExceptionType2
} // … more catch blocks as needed
catch (...) {
// Catch-all handler for other unhandled exceptions
std::cerr << "Unknown exception caught" << std::endl;
}
1//filename: divisionzero.cpp
2//complile: g++ -std=c++17 -o divisionzero divisionzero.cpp
3//execute: ./divisionzero
4#include <iostream>
5
6int main(int argc, char *argv[]) {
7 try {
8
9 int divisor = 0;
10 if (divisor == 0) {
11 throw std::runtime_error("Division by zero exception"); // Note: throwing a value object here
12 }
13 int result = 10 / divisor; // this statement never executes. Creates a "float-point" exception, but outside of C++
14 // can not detect this error
15 }
16 catch (const std::runtime_error &e) { // note output to cout instead of cerr to display within docable.
17 std::cout << "Runtime error: " << e.what() << "\n";
18 return EXIT_FAILURE;
19 } catch (...) {
20 std::cout << "Unknown exception caught" << "\n";
21 return EXIT_FAILURE;
22 }
23 return EXIT_SUCCESS;
24}
As you can see in the example, when we detect an exceptional condition, we “throw” an exception with the throw
keyword:
throw runtime_error("A problem occurred.");
Here, runtime_error
is a standard exception type provided by the C++ Standard Library. We can throw objects of any data type as exceptions, including built-in, custom, or library types. For example, the C++ STL throws an out_of_range
exception in the vector<>:at()
method if the provided index is invalid.
One of the nuances when converting string values is that the parser will stop considering input when an invalid character is encountered. As such, if you want to ensure that the entire string has been processed, we need to check how many characters were processed as compared to the length of the string. Note: With modern C++, we do not need to indicate that a function throws an exception - we do use the keyword noexcept
if the function is guaranteed not throw an exception.
1//filename: convert.cpp
2//complile: g++ -std=c++17 -o convert convert.cpp
3//execute: ./convert
4#include <iostream>
5#include <string>
6
7int convertInt(std::string s) {
8 std::size_t idx = 0;
9 int result = std::stoi(s,&idx);
10 if (idx != s.size()) {
11 throw std::invalid_argument("unprocessed input: "+s);
12 }
13 return result;
14}
15
16int main() {
17 try {
18 std::cout << convertInt("100") << "\n";
19 std::cout << convertInt("100.4") << "\n";
20 } catch (const std::invalid_argument& a) {
21 std::cerr << "Invalid argument: " << a.what() << "\n";
22 return EXIT_FAILURE;
23 } catch (const std::out_of_range& r) {
24 std::cerr << "out of range of double: " << r.what() << "\n";
25 return EXIT_FAILURE;
26 }
27 return EXIT_SUCCESS;
28}
8.1. Exception Propagation#
If an exception is thrown but not caught in a particular scope, the exception propagates up to higher levels of the call stack until it is caught or until it reaches main
. If it gets to main
without being caught, the program will terminate. As the exception propagates through the call stack, those corresponding functions exit/go out of scope. Any declared objects within those functions will have their corresponding destructors called.
8.2. Standard Exceptions#
The C++ library defines several exceptions used to report issues within the library. As from above, the library can detect these situations, but separate code must exist to handle and recover from these exceptions. Here are some of these defined exceptions:
Exception Class |
Purpose |
---|---|
|
base class - most general kind of problem |
|
represents problems that can only be detected at runtime. |
|
Computation overflowed (number too great for the underlying type) |
|
occurs when a floating-point operation results in a value that is closer to zero than the smallest representable positive value for the data type being used, and the value cannot be represented accurately. |
|
thrown when an argument value is out of the valid range. |
|
thrown when an invalid argument is passed to a function. |
These classes are defined in stdexcept
- you will need to include that header to be able to reference these classes.
While these errors have been defined, C++ often leaves it to the program to detect these situations and throw the appropriate exception.
8.3. Notes#
Do not throw exceptions from destructors, as this can cause unexpected behavior. (Destructors will be covered in classes.)
Use exceptions for exceptional, non-routine error conditions.
Catch exceptions by reference (preferably
const
reference). This provides a number of benefits:allows us to use polymorphic behavior when accessing the exceptions (polymorphism will be covered later)
signifies that will not change the exception object.
prevents “object slicing” when a copy of the exception is made if the exception is not a reference. Entering into a catch block functions similarly to calling a function. Object slicing occurs when an object of a subclass is assigned to an instance of a base class - we lose access to the state and behavior defined in the subclass. The copy function in the base class only knows about its state, not those of any subclasses.