std::terminate
, std::set_terminate
, and std::abort
.In this lesson, we’ll begin to see how exception handling is helpful in larger programs, due to its close connection to the call stack.
In previous examples, our catch
statements were always catching exceptions from a function that was directly called within the corresponding try
block:
void SomeFunction() {
throw std::runtime_error;
}
void AnotherFunction() {
try {
SomeFunction();
} catch (...) {
std::cout << "Caught!";
}
}
But, this need not be the case. The error can be caught right away, in the same stack frame:
void SomeFunction() {
try {
throw std::runtime_error;
} catch (...) {
std::cout << "Caught!";
}
}
More usefully, the exception can be caught in any underlying stack frame:
void SomeFunction () {
throw std::runtime_error;
}
void AnotherFunction() {
SomeFunction();
}
int main() {
try {
AnotherFunction();
} catch (...) {
std::cout << "Caught!";
}
}
This is the true power of exceptions - it gives us the ability to handle errors in a flexible and intuitive way.
throw
and Unreachable CodeWhen a throw
expression is encountered during the running of our application, the program immediately exits the current try
block and searches for the closest catch
statement that can handle the type of exception thrown.
This is demonstrated in the following example:
void MyFunction() {
try {
throw std::runtime_error;
std::cout << "A"; // Unreachable
} catch (...) {
std::cout << "B";
}
std::cout << "C";
}
BC
The throw
on line 3 makes the rest of the try
block unreachable, so “A” is not logged. The catch
block in this function can handle the type of exception thrown, so “B” is logged out.
Finally, because our function recovered, execution continues, and “C” is logged out as normal.
Because of this behavior, exceptions can be a common cause of memory leaks. This simple program has a memory leak if SomeFunction
can ever throw an exception:
void MyFunction() {
int* MyInt { new Int { 2 } };
SomeFunction();
delete MyInt;
}
This is yet another reason to use smart pointers and the other techniques covered in the chapter on memory.
If the exception is not handled within the function that threw it, that function’s stack frame is immediately popped off the stack. Here are three examples of where that would happen:
void MyFunction() {
// Uncaught exception
throw std::runtime_error;
// This stack frame will be popped before reaching here
std::cout << "I'm unreachable";
}
void MyFunction() {
try {
// ...
} catch (...) {
// ...
}
// Exception thrown outside a try block
throw std::runtime_error;
// This stack frame will be popped before reaching here
std::cout << "I'm unreachable";
}
void MyFunction() {
// No catch block matching the type of exception thrown
try {
throw std::runtime_error;
} catch (std::logic_error& e) {
// ...
}
// This stack frame will be popped before reaching here
std::cout << "I'm unreachable";
}
In all three cases, the MyFunction
stack frame is popped off the stack. From there, the search for an appropriate catch
statement continues on the next stack frame - the function that called our function:
void CallingFunction() {
// Is there a try block wrapping this,
// with an appropriate catch?
MyFunction();
// If not, I will be popped off the stack, and
// the search will continue at the next level
// of the stack - the function that called me
}
If the exception thrown from the call to MyFunction
wasn’t caught in CallingFunction
, it too is popped off the stack, and the search continues at the function that called CallingFunction
.
This process repeats until an appropriate catch
is found. If the process reaches main
, and that too is unable to handle the exception, the program terminates.
This recursive process is commonly referred to as unwinding the stack.
std::terminate()
When our application has an unhandled exception, it terminates.
However, we can also trigger this termination ourselves, using std::terminate
, available from the <exception>
header.
This is mostly useful if our code encounters an error it knows is irrecoverable, and wants to close as quickly as possible, bypassing any possible catch
statements that might attempt recovery.
#include <exception>
#include <iostream>
void MyFunction() { std::terminate(); }
int main() {
try {
MyFunction();
} catch (...) {
std::cout << "Caught!";
}
}
C:\examples\exceptions.exe (process 69992) exited with code 3.
Note how “Caught!” is not logged
std::set_terminate()
and std::abort()
By default, std::terminate
simply calls another function, std::abort()
, to close our application.
However, we can define our program’s terminate handler ourselves, by passing a function to std::set_terminate
.
We cannot attempt to recover from within a terminate handler. Our custom handler still needs to call std::abort()
, but it doesn’t have to call it immediately - we can do other things first.
#include <exception>
#include <iostream>
void MyFunction() { throw 1; }
int main() {
std::set_terminate([]() {
std::cout << "Goodbye!";
std::abort();
});
MyFunction();
}
Goodbye!
C:\examples\exceptions.exe (process 69992) exited with code 3.
In large-scale projects, terminate handlers are most commonly used to generate reports containing additional information about the crash, which may help us debug.
These are typically called crash dumps, and they include information like what the unhandled exception was, the value of important variables, the call stack, hardware configuration, and more.
noexcept
FunctionsWithin our program, we can mark functions as noexcept
:
void SomeFunction() noexcept {
// ...
}
These functions should not throw an exception to their caller. Note, we can still throw exceptions within these functions, or in the functions they call. The noexcept
specifier is just stating that an exception will not be thrown to the caller.
If a noexcept
function does throw an exception to the caller, std::terminate
will be called, bypassing any attempts to catch that exception.
Using noexcept
correctly has 3 potential benefits:
noexcept
functionIt is possible to pass a boolean to the noexcept
specifier, to determine whether or not it applies.
For example, noexcept(true)
is equivalent to noexcept
and noexcept(false)
is equivalent to not having the specifier there at all.
We can pass any boolean expression to this handler, as long as it can be evaluated at compile time.
constexpr bool SomeBoolean{true};
void SomeFunction() noexcept(SomeBoolean) {
// ...
}
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.