std::terminate
and the noexcept
specifierstd::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:
std::terminate()
is and when it is used.std::terminate()
using std::set_terminate()
.noexcept
specifier and its impact on functions.noexcept
in move semantics, and the std::move_if_noexcept()
function.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:
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.
The previous example is the first time in the course that we’ve provided a function (CustomTerminate
) as an argument to another function (std::set_terminate
).
We can do this, as C++ implements a programming paradigm known as first-class functions. This concept lets us use functions in much the same way we can any other type. For example, functions can be stored in variables, passed to other functions, and returned from functions.
We have a dedicated chapter that covers these mechanisms in more detail later in the course.
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::abort();
}
int main() {
std::set_terminate(CustomTerminate);
throw 1;
}
Goodbye!
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.
noexcept
SpecifierSimilar 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:
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.
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.
In our earlier lesson on move semantics, we introduced the idea of the move constructor and move assignment operator.
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.
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 Constructor
However, 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 Constructor
If 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 Constructor
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:
noexcept
everywhere it is correct, ornoexcept
only where it is usefulWe 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.
noexcept
with Data Structures and AlgorithmsImplementing 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.
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 Constructor
Adding 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 Constructor
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 invoke std::terminate
directly and how it interacts with unhandled exceptions.std::set_terminate
: We explored customizing the termination process using std::set_terminate()
, allowing for additional actions like logging or cleanup before terminating.noexcept
: A specifier that ensures a function does not throw exceptions to its caller, which is enforced at run time. If a noexcept
function leaks an exception, our program terminates.noexcept
in Move Semantics: We examined how noexcept
is particularly useful in move semantics, particularly with std::move_if_noexcept
.std::terminate
and the noexcept
specifierThis lesson explores the std::terminate
function and noexcept
specifier, with particular focus on their interactions with move semantics.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.