First, let's briefly review how C++ exceptions are thrown and caught. In the following example, we will throw an exception from a function and then catch the exception in our main() function:
#include <iostream>
#include <stdexcept>
void foo()
{
throw std::runtime_error("The answer is: 42");
}
int main(void)
{
try {
foo();
}
catch(const std::exception &e) {
std::cout << e.what() << '\n';
}
return 0;
}
As shown in the preceding example, we created a function called foo() that throws an exception. This function is called in our main() function inside a try/catch block, which is used to catch any exceptions that might be thrown by the code executed inside the try block, which in this case is the foo() function. When the exception is thrown by the foo() function, it is successfully caught and outputted to stdout.
All of this works because we did not add the noexcept specifier to the foo() function. By default, a function is allowed to throw an exception, just as we did in this example. In some cases, however, we do not want to allow exceptions to be thrown, depending on how we expect a function to execute. Specifically, how a function handles exceptions can be defined as the following (known as exception safety):
- No-throw guarantee: The function cannot throw an exception, and if an exception is thrown internally, the exception must be caught and handled, including allocation failures.
- Strong exception safety: The function can throw an exception, and if an exception is thrown, any state that was modified by the function is rolled back or undone with no side effects.
- Basic exception safety: The function can throw an exception, and if an exception is thrown, any state that was modified by the function is rolled back or undone, but side effects are possible. It should be noted that these side effects do not include invariants, meaning the program is in a valid, non-corrupted state.
- No exception safety: The function can throw an exception, and if an exception is thrown, the program could enter a corrupted state.
In general, if a function has a no-throw guarantee, it is labeled with noexcept; otherwise, it is not. An example of why exception safety is so important is with std::move. For example, suppose we have two instances of std::vector and we wish to move one vector into another. To perform the move, std::vector might move each element of the vector from one instance to the other. If the object is allowed to throw when it is moved, the vector could end up with an exception in the middle of the move (that is, half of the objects in the vector are moved successfully). When the exception occurs, std::vector would obviously attempt to undo the moves that it has already performed by moving these back to the original vector before returning the exception. The problem is, attempting to move the objects back would require std::move(), which could throw and exception again, resulting in a nested exception. In practice, moving one std::vector instance to another doesn't actually perform an object-by-object move, but resizing does, and, in this specific issue, the standard library requires the use of std::move_if_noexcept to handle this situation to provide exception safety, which falls back to a copy when the move constructor of an object is allowed to throw.
The noexcept specifier is used to overcome these types of issues by explicitly stating that the function is not allowed to throw an exception. This not only tells the user of the API that they can safely use the function without fear of an exception being thrown and potentially corrupting the execution of the program, but it also forces the author of the function to safely handle all possible exceptions or call std::terminate(). Although noexcept, depending on the compiler, also provides optimizations by reducing the overall size of the application when defined, its main use is to state the exception safety of a function such that other functions can reason about how a function will execute.
In the following example, we add the noexcept specifier to our foo() function defined earlier:
#include <iostream>
#include <stdexcept>
void foo() noexcept
{
throw std::runtime_error("The answer is: 42");
}
int main(void)
{
try {
foo();
}
catch(const std::exception &e) {
std::cout << e.what() << '\n';
}
return 0;
}
When this example is compiled and executed, we get the following:
As shown in the preceding example, the noexcept specifier was added, which tells the compiler that foo() is not allowed to throw an exception. Since, however, the foo() function does throw an exception, when it is executed, std::terminate() is called. In fact, in this example, std::terminate() will always be called, which is something the compiler is able to detect and warn about.
Calling std::terminate() is obviously not the desired outcome of a program. In this specific case, since the author has labeled the function as noexcept, it is up to the author to handle all possible exceptions. This can be done as follows:
#include <iostream>
#include <stdexcept>
void foo() noexcept
{
try {
throw std::runtime_error("The answer is: 42");
}
catch(const std::exception &e) {
std::cout << e.what() << '\n';
}
}
int main(void)
{
foo();
return 0;
}
As shown in the preceding example, the exception is wrapped in a try/catch block to ensure the exception is safely handled before the foo() function completes its execution. Also, in this example, only exceptions that originate from std::exception() are caught. This is the author's way of saying which types of exceptions can be safely handled. If, for example, an integer was thrown instead of std::exception(), std::terminate() would still be executed automatically since noexcept was added to the foo() function. In other words, as the author, you are only required to handle the exceptions that you can, in fact, safely handle. The rest will be sent to std::terminate() for you; just understand that, by doing this, you change the exception safety of the function. If you intend for a function to be defined with a no-throw guarantee, the function cannot throw an exception at all.
It should also be noted that if you mark a function as noexcept, you need to not only pay attention to exceptions that you throw but also to the functions that may throw themselves. In this case, std::cout is being used inside the foo() function, which means the author has to either knowingly ignore any exceptions that std::cout could throw, which would result in a call to std::terminate() (which is what we are doing here), or the author needs to identify which exceptions std::cout could throw and attempt to safely handle them, including exceptions such as std::bad_alloc.
The std::vector.at() function throws an std::out_of_range() exception if the provided index is out of bounds with respect to the vector. In this case, the author can catch this type of exception and return a default value, allowing the author to safely mark the function as noexcept.
The noexcept specifier is also capable of acting as a function, taking a Boolean expression, as in the following example:
#include <iostream>
#include <stdexcept>
void foo() noexcept(true)
{
throw std::runtime_error("The answer is: 42");
}
int main(void)
{
try {
foo();
}
catch(const std::exception &e) {
std::cout << e.what() << '\n';
}
return 0;
}
This results in the following when executed:
As shown in the preceding example, the noexcept specifier was written as noexcept(true). If the expression evaluates to true, it is as if noexcept was provided. If the expression evaluates to false, it is as if the noexcept specifier was left out, allowing exceptions to be thrown. In the preceding example, the expression evaluates to true, which means that the function is not allowed to throw an exception, which results in std::terminate() being called when foo() throws an exception.
Let's look at a more complicated example to demonstrate how this can be used. In the following example, we will create a function called foo() that will shift an integer value by 32 bits and cast the result to a 64-bit integer. This example will be written using template metaprogramming, allowing us to use this function on any integer type:
#include <limits>
#include <iostream>
#include <stdexcept>
template<typename T>
uint64_t foo(T val) noexcept(sizeof(T) <= 4)
{
if constexpr(sizeof(T) <= 4) {
return static_cast<uint64_t>(val) << 32;
}
throw std::runtime_error("T is too large");
}
int main(void)
{
try {
uint32_t val1 = std::numeric_limits<uint32_t>::max();
std::cout << "foo: " << foo(val1) << '\n';
uint64_t val2 = std::numeric_limits<uint64_t>::max();
std::cout << "foo: " << foo(val2) << '\n';
}
catch(const std::exception &e) {
std::cout << e.what() << '\n';
}
return 0;
}
This results in the following when executed:
As shown in the preceding example, the issue with the foo() function is that if the user provides a 64-bit integer, it cannot shift by 32 bits without generating an overflow. If the integer provided, however, is 32 bits or less, the foo() function is perfectly safe. To implement the foo() function, we used the noexcept specifier to state that the function is not allowed to throw an exception if the provided integer is 32 bits or less. If the provided integer is greater than 32 bits, an exception is allowed to throw, which, in this case, is an std::runtime_error() exception stating that the integer is too large to be safely shifted.