Using std::terminate() and the noexcept Specifier
This lesson explores the std::terminate() function and noexcept specifier, with particular focus on their interactions with move semantics.
In this lesson, we will delve into two important aspects of error handling: the std::terminate() function and the noexcept specifier. We will explore:
- What
std::terminate()is and when it is used. - How to customize the behavior of
std::terminate()usingstd::set_terminate(). - The significance of the
noexceptspecifier and its impact on functions. - Practical implications of
noexceptin move semantics, and thestd::move_if_noexcept()function.
Using std::terminate()
As we've seen in some previous examples, when our application has an unhandled exception, it terminates. "Terminate" has a specific meaning in this context - it refers to invoking the std::terminate() function.
We don't need to be using exceptions to use this function - we can just call it directly after including the <exception> header:
#include <exception>
void MyFunction() {
std::terminate();
}Calling std::terminate() will bypass any possible catch statements that might attempt recovery.
#include <exception>
#include <iostream>
void MyFunction() {
std::terminate();
}
int main() {
try {
MyFunction();
} catch (...) {
std::cout << "Caught!";
}
}Note that our catch block was not executed:
example.exe (process 69992) exited with code 3.std::terminate() is designed for when our code encounters an error it knows is irrecoverable, meaning the process should just end. From our output, we can note that this is reflected by our exit code. Any non-zero exit code indicates an error:
Using std::set_terminate()
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().
Below, we define a function called CustomTerminate(). We then pass it to std::set_terminate(), registering this function as our terminate handler:
#include <exception>
#include <iostream>
void CustomTerminate() {
std::abort();
}
int main() {
std::set_terminate(CustomTerminate);
throw 1;
}example.exe (process 69992) exited with code 3.Within our terminate handler, we can't try to recover and keep our program running. Our custom handler still needs to call std::abort().
However, it doesn't have to call std::abort() immediately. We can implement any behaviour we need before closing our program:
#include <exception>
#include <iostream>
void CustomTerminate() {
std::cout << "Terminating!" << std::flush;
std::abort();
}
int main() {
std::set_terminate(CustomTerminate);
throw 1;
}Terminating!
example.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.
They can include information like what the unhandled exception was, the value of important variables, the call stack, hardware configuration, and more.
The noexcept Specifier
Similar to how we can mark functions with annotations like const and override, so too can we mark them noexcept:
void SomeFunction() noexcept {
// ...
}This specifier tells the compiler (and other developers) that this function will not throw any exceptions to their caller. This means either:
- no exceptions will be thrown, or
- any exceptions that are thrown will be handled internally, without leaking up to the caller
This is enforced at run time, rather than compile time. If a noexcept function leaks an exception, our program will std::terminate(), rather than giving the caller the opportunity to catch the exception.
The noexcept specifier can be applied to any function where we want this behavior. However, its most important use case, and the reason it was originally added to the language, is to solve a specific problem at the intersection of exceptions and move semantics. We cover this problem in the next section.
Conditional noexcept
It 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) {
// ...
}This is most often used in template code, where whether a function is noexcept or not depends on the types our template was instantiated with.
Exceptions and Move Semantics
In our earlier lesson on move semantics, we introduced the idea of the move constructor and move assignment operator.
Move Semantics
Learn how we can improve the performance of our types using move constructors, move assignment operators and std::move()
These functions let us define how our objects can be moved to another location. They are designed to be fast, but they leave the original object in an indeterminate state, often called the moved-from state.
In most cases, this isn't a problem, because once the move completes, we're not going to be using the original object anymore. However, what if the move operator or constructor throws an exception, mid-way through the process?
At the point the exception is thrown, the original object may already have been compromised, but the new object hasn't fully been created yet. So, both of the objects are in an indeterminate state.
To handle this scenario, alongside move semantics in C++11, the language also introduced the noexcept specifier and the move_if_noexcept() function.
Using std::move_if_noexcept()
In our move semantics lesson, we saw how we could use std::move() to cast an object to an rvalue reference. This is how we indicate an object can be moved-from, allowing the surrounding expression to use move semantics.
Below, we create a MyType with such a reference, prompting the more efficient move constructor to be used:
#include <iostream>
struct MyType {
MyType() = default;
MyType(const MyType& Other) {
std::cout << "Copy Constructor";
}
MyType(MyType&& Other) {
std::cout << "Move Constructor";
}
};
int main() {
MyType Original;
MyType New{std::move(Original)};
}Move ConstructorHowever, this move constructor does not specify noexcept. This means that it may throw an exception, which the calling function (main, in this case) will have to deal with.
Given the complexities of dealing with exceptions during move operations we discussed earlier, the calling function probably won't want to deal with that.
Instead, it can switch to use std::move_if_noexcept(). This function will only cast our object to an rvalue reference if the move constructor is marked noexcept.
In our case, it isn't, so our program falls back to the safer copy constructor:
#include <iostream>
struct MyType {
MyType() = default;
MyType(const MyType& Other) {
std::cout << "Copy Constructor";
}
MyType(MyType&& Other) {
std::cout << "Move Constructor";
}
};
int main() {
MyType Original;
MyType New{std::move_if_noexcept(Original)};
}Copy ConstructorIf we correctly mark our move constructor as noexcept, our move_if_noexcept() example switches back to using move semantics as we'd expect:
#include <iostream>
struct MyType {
MyType() = default;
MyType(const MyType& Other) {
std::cout << "Copy Constructor";
}
MyType(MyType&& Other) noexcept {
std::cout << "Move Constructor";
}
};
int main() {
MyType Original;
MyType New{std::move_if_noexcept(Original)};
}Move ConstructorWhen to Apply noexcept
In general, most of the functions we write won't be throwing exceptions to their caller. So should we mark all of these functions noexcept?
As usual, opinions differ, and different teams will have their guidelines on what approach to take. They broadly fall into one of two camps:
- Apply
noexcepteverywhere it is correct, or - Apply
noexceptonly where it is useful
We use the second approach in this course. In general, this means we apply noexcept to almost every implementation of move semantics, and pretty much nowhere else.
Using noexcept with Data Structures and Algorithms
Implementing move semantics with noexcept is useful even if we're not using it directly. This is because, behind the scenes, many of the data structures and algorithms we use will be checking for noexcept, and only using our move semantics if it is applied.
For example, when std::vector resizes, it needs to move all of our objects to a new location.
Dynamic Arrays using std::vector
Explore the fundamentals of dynamic arrays with an introduction to std::vector
This is implemented using std::move_if_noexcept() meaning that, if our move constructor isn't marked noexcept, we won't get the benefit of having implemented it.
Below, we create a std::vector with one object, then prompt the vector to move to a new location using reserve().
Because our move constructor isn't marked noexcept, the vector doesn't use it when moving our object, preferring the slower copy constructor:
#include <iostream>
#include <vector>
struct MyType {
MyType() = default;
MyType(const MyType& Other) {
std::cout << "Copy Constructor";
}
MyType(MyType&& Other) { // missing noexcept
std::cout << "Move Constructor";
}
};
int main() {
std::vector<MyType> V;
V.emplace_back();
V.reserve(100);
}Copy ConstructorAdding noexcept to our move constructor now means the standard library algorithm will use it:
#include <iostream>
#include <vector>
struct MyType {
MyType() = default;
MyType(const MyType& Other) {
std::cout << "Copy Constructor";
}
MyType(MyType&& Other) noexcept {
std::cout << "Move Constructor";
}
};
int main() {
std::vector<MyType> V;
V.emplace_back();
V.reserve(100);
}Move ConstructorSummary
In this lesson, we took an in-depth look at std::terminate() and the noexcept specifier. Key takeaways include:
std::terminate(): A function for handling irrecoverable errors, terminating the program in a controlled manner. We learned how to invokestd::terminate()directly and how it interacts with unhandled exceptions.- Custom Termination with
std::set_terminate(): We explored customizing the termination process usingstd::set_terminate(), allowing for additional actions like logging or cleanup before terminating. - The Role of
noexcept: A specifier that ensures a function does not throw exceptions to its caller, which is enforced at run time. If anoexceptfunction leaks an exception, our program terminates. noexceptin Move Semantics: We examined hownoexceptis particularly useful in move semantics, particularly withstd::move_if_noexcept().
Storing and Rethrowing Exceptions
This lesson offers a comprehensive guide to storing and rethrowing exceptions