Exception specifications and noexcept

Looking at a typical function declaration, there's no way to determine whether a function might throw an exception:

void processRequest(); // does this function throw exceptions or not?

In the above example, can processRequest() throw an exception? There's no indication. But the answer matters in certain contexts. In the lesson on exception dangers, we described how an exception thrown from a destructor during stack unwinding causes program termination. If processRequest() can throw an exception, calling it from a destructor (or anywhere else where thrown exceptions are problematic) is risky. Although we could wrap calls to processRequest() in try-catch blocks, we have to remember to do this consistently, and we need to handle all possible exception types that might be thrown.

While comments might document whether a function throws exceptions (and if so, what kind), documentation can become outdated and there's no compiler enforcement for comments.

Exception specifications are a language mechanism originally designed to document what kinds of exceptions a function might throw as part of its specification. While most exception specifications have been deprecated or removed, one useful specification was added as a replacement, which we'll cover in this lesson.

The noexcept specifier

In C++, all functions are classified as either non-throwing or potentially throwing. A non-throwing function promises not to throw exceptions that are visible to the caller. A potentially throwing function may throw exceptions that are visible to the caller.

To define a function as non-throwing, we use the noexcept specifier. We place the noexcept keyword in the function declaration, to the right of the parameter list:

void processRequest() noexcept; // this function is specified as non-throwing

Note that noexcept doesn't prevent the function from throwing exceptions or calling potentially throwing functions. This is allowed as long as the noexcept function catches and handles those exceptions internally, preventing them from exiting the function.

If an unhandled exception would exit a noexcept function, std::terminate will be called (even if an exception handler exists somewhere up the stack that would otherwise handle the exception). When std::terminate is called from inside a noexcept function, stack unwinding may or may not occur (depending on implementation and optimizations), which means objects may or may not be destructed properly before termination.

Key Concept
The promise that a noexcept function makes to not throw exceptions visible to the caller is a contractual promise, not a promise enforced by the compiler. So while calling a noexcept function should be safe, any exception handling bugs in the noexcept function that break the contract will result in program termination! This shouldn't happen, but bugs happen.

For this reason, it's best that noexcept functions don't work with exceptions at all, or call potentially throwing functions that could raise an exception. A noexcept function can't have an exception handling bug if no exceptions can possibly be raised in the first place!

Much like functions that differ only in return values cannot be overloaded, functions differing only in their exception specification cannot be overloaded.

**Illustrating the behavior of noexcept functions and exceptions**

The following program illustrates the behavior of noexcept functions and exceptions in various scenarios:

// h/t to reader yellowEmu for the first draft of this program
#include <iostream>

class Resource
{
public:
    ~Resource()
    {
        std::cout << "Resource destroyed\n";
    }
};

void raiseError()
{
    std::cout << "Throwing exception\n";
    throw std::runtime_error{"Critical error"};
}

void potentiallyThrowing()
{
    std::cout << "potentiallyThrowing() called\n";
    Resource resource{};
    raiseError();
    std::cout << "This never prints\n";
}

void nonThrowing() noexcept
{
    std::cout << "nonThrowing() called\n";
    Resource resource{};
    raiseError();
    std::cout << "This never prints\n";
}

void safeFunction(int testCase) noexcept
{
    std::cout << "safeFunction() test case " << testCase << " called\n";
    try
    {
        (testCase == 1) ? potentiallyThrowing() : nonThrowing();
    }
    catch (...)
    {
        std::cout << "safeFunction() caught exception\n";
    }
}

int main()
{
    std::cout << std::unitbuf; // flush buffer after each insertion
    std::cout << std::boolalpha; // print boolean as true/false

    safeFunction(1);
    std::cout << "Test successful\n\n";

    safeFunction(2);
    std::cout << "Test successful\n";

    return 0;
}

When tested, this program printed:

safeFunction() test case 1 called potentiallyThrowing() called Throwing exception Resource destroyed safeFunction() caught exception Test successful

safeFunction() test case 2 called nonThrowing() called Throwing exception terminate called after throwing an instance of 'std::runtime_error' what(): Critical error

and then the program aborted.

Let's explore what's happening in detail. Note that safeFunction is a noexcept function, and thus promises not to expose any exception to the caller (main).

The first test case illustrates that noexcept functions can call potentially throwing functions and handle any exceptions those functions throw. First, safeFunction(1) is called, which calls potentially throwing function potentiallyThrowing, which calls raiseError, which throws an exception. The first handler for this exception is in safeFunction, so the exception unwinds the stack (destroying local variable resource in the process), and the exception is caught and handled within safeFunction. Because safeFunction doesn't expose this exception to the caller (main), there's no violation of noexcept, and control returns to main.

The second test case illustrates what happens when a noexcept function tries to pass an exception back to its caller. First, safeFunction(2) is called, which calls non-throwing function nonThrowing, which calls raiseError, which throws an exception. The first handler for this exception is in safeFunction. However, nonThrowing is noexcept, and to get to the handler in safeFunction, the exception would have to propagate to the caller of nonThrowing. That violates the noexcept contract of nonThrowing, so std::terminate is called, and our program aborts immediately. When tested, the stack was not unwound (as illustrated by resource not being destroyed).

The noexcept specifier with a Boolean parameter

The noexcept specifier has an optional Boolean parameter. noexcept(true) is equivalent to noexcept, meaning the function is non-throwing. noexcept(false) means the function is potentially throwing. These parameters are typically only used in template functions, so that a template function can be dynamically created as non-throwing or potentially throwing based on some parameterized value.

Which functions are non-throwing and potentially-throwing

Functions that are implicitly non-throwing:

  • Destructors

Functions that are non-throwing by default for implicitly-declared or defaulted functions:

  • Constructors: default, copy, move
  • Assignments: copy, move
  • Comparison operators (as of C++20)

However, if any of these functions call (explicitly or implicitly) another function which is potentially throwing, then the listed function will be treated as potentially throwing as well. For example, if a class has a data member with a potentially throwing constructor, then the class's constructors will be treated as potentially throwing. As another example, if a copy assignment operator calls a potentially throwing assignment operator, then the copy assignment will be potentially throwing.

Functions that are potentially throwing (if not implicitly-declared or defaulted):

  • Normal functions
  • User-defined constructors
  • User-defined operators

The noexcept operator

The noexcept operator can also be used inside expressions. It takes an expression as an argument and returns true or false depending on whether the compiler thinks it will throw an exception. The noexcept operator is checked statically at compile-time and doesn't actually evaluate the input expression.

void performTask() { throw std::runtime_error{"Task failed"}; }
void executeCommand() {}
void safeOperation() noexcept {}
struct Settings {};

constexpr bool b1{ noexcept(7 + 3) }; // true; ints are non-throwing
constexpr bool b2{ noexcept(performTask()) }; // false; performTask() throws an exception
constexpr bool b3{ noexcept(executeCommand()) }; // false; executeCommand() is implicitly noexcept(false)
constexpr bool b4{ noexcept(safeOperation()) }; // true; safeOperation() is explicitly noexcept(true)
constexpr bool b5{ noexcept(Settings{}) }; // true; a struct's default constructor is noexcept by default

The noexcept operator can be used to conditionally execute code depending on whether it is potentially throwing or not. This is required to fulfill certain exception safety guarantees, which we'll discuss next.

Exception safety guarantees

An exception safety guarantee is a contractual guideline about how functions or classes will behave when exceptions occur. There are four levels of exception safety guarantees:

  • No guarantee - There are no guarantees about what will happen if an exception is thrown (e.g., a class may be left in an unusable state)
  • Basic guarantee - If an exception is thrown, no memory will be leaked and the object is still usable, but the program may be left in a modified state.
  • Strong guarantee - If an exception is thrown, no memory will be leaked and the program state will not be changed. This means the function must either completely succeed or have no side effects if it fails. This is easy if failure happens before anything is modified, but can also be achieved by rolling back any changes so the program returns to the pre-failure state.
  • No throw / No fail guarantee - The function will always succeed (no-fail) or fail without throwing an exception exposed to the caller (no-throw). Exceptions may be thrown internally if not exposed. The noexcept specifier maps to this level of exception safety guarantee.

Let's examine the no-throw/no-fail guarantees in more detail:

The no-throw guarantee: if a function fails, it won't throw an exception. Instead, it will return an error code or ignore the problem. No-throw guarantees are required during stack unwinding when an exception is already being handled; for example, all destructors should have a no-throw guarantee (as should any functions those destructors call). Examples of code that should be no-throw:

  • destructors and memory deallocation/cleanup functions
  • functions that higher-level no-throw functions need to call

The no-fail guarantee: a function will always succeed in what it tries to do (and thus never needs to throw an exception, making no-fail a slightly stronger form of no-throw). Examples of code that should be no-fail:

  • move constructors and move assignment (move semantics, covered in chapter 22)
  • swap functions
  • clear/erase/reset functions on containers
  • operations on std::unique_ptr (also covered in chapter 22)
  • functions that higher-level no-fail functions need to call

When to use noexcept

Just because your code doesn't explicitly throw any exceptions doesn't mean you should start adding noexcept everywhere. By default, most functions are potentially throwing, so if your function calls other functions, there's a good chance it calls a potentially throwing function, making it potentially throwing too.

There are a few good reasons to mark functions as non-throwing:

  • Non-throwing functions can be safely called from functions that are not exception-safe, such as destructors
  • Functions that are noexcept can enable compiler optimizations that would not otherwise be available. Because a noexcept function cannot throw an exception outside the function, the compiler doesn't have to worry about keeping the runtime stack in an unwindable state, which can allow it to produce faster code.
  • There are significant cases where knowing a function is noexcept allows us to produce more efficient implementations: the standard library containers (such as std::vector) are noexcept aware and will use the noexcept operator to determine whether to use move semantics (faster) or copy semantics (slower) in some places. We cover move semantics in chapter 22, and this optimization in lesson 27.10.

The standard library's policy is to use noexcept only on functions that must not throw or fail. Functions that are potentially throwing but don't actually throw exceptions (due to implementation) typically are not marked as noexcept.

For your own code, always mark the following as noexcept:

  • Move constructors
  • Move assignment operators
  • Swap functions

For your code, consider marking the following as noexcept:

  • Functions for which you want to express a no-throw or no-fail guarantee (e.g., to document they can be safely called from destructors or other noexcept functions)
  • Copy constructors and copy assignment operators that are no-throw (to take advantage of optimizations)
  • Destructors. Destructors are implicitly noexcept as long as all members have noexcept destructors
Best Practice
Always make move constructors, move assignment, and swap functions `noexcept`.

Make copy constructors and copy assignment operators noexcept when you can.

Use noexcept on other functions to express a no-fail or no-throw guarantee.

Best Practice
If you are uncertain whether a function should have a no-fail/no-throw guarantee, err on the side of caution and do not mark it with `noexcept`. Reversing a decision to use noexcept violates an interface commitment to the user about the behavior of the function and may break existing code. Making guarantees stronger by later adding noexcept to a function that was not originally noexcept is considered safe.

Summary

The noexcept specifier: Functions can be marked as non-throwing using the noexcept specifier. A non-throwing function promises not to throw exceptions visible to the caller. If an unhandled exception would exit a noexcept function, std::terminate is called.

Contractual promise: The noexcept promise is contractual, not compiler-enforced. Any exception handling bugs in a noexcept function that break the contract result in program termination.

The noexcept operator: The noexcept operator takes an expression and returns true or false at compile-time depending on whether the compiler thinks it will throw an exception. This enables conditional code execution based on exception guarantees.

Exception safety guarantees: Four levels exist: no guarantee, basic guarantee (no leaks, object usable), strong guarantee (no leaks, no state change), and no-throw/no-fail guarantee (always succeeds or fails without exposing exceptions).

When to use noexcept: Always mark move constructors, move assignment, and swap functions as noexcept. Consider marking copy constructors, copy assignment, and destructors as noexcept when appropriate. Use noexcept to express no-throw or no-fail guarantees.

Compiler optimizations: Functions marked noexcept enable compiler optimizations that wouldn't otherwise be available. The standard library containers use the noexcept operator to determine whether to use move semantics (faster) or copy semantics (slower) in some operations.

Dynamic exception specifications: Before C++11, dynamic exception specifications used the throw keyword to list exception types. These were deprecated in C++11 and removed in C++17/C++20 due to various issues.

The noexcept specifier provides a standardized way to document and enforce exception guarantees, enabling both compiler optimizations and clearer interfaces for exception-safe code.