03 - exception specification

Dynamic exception specification

Until C++17, functions (including special member functions) could have dynamic exception specification.

1
2
3
4
5
6
7
8
// may throw std::runtime_error, std::bad_alloc or something derived from these
void f1() throw(std::runtime_error, std::bad_alloc);

// never throws any exceptions
void f2() throw();

// equivalent to the above, the preferred way since C++11
void f3() noexcept;

This informed the compiler which types of exceptions (or types derived from) can be thrown (otherwise each function is assumed to potentially throw everything). From cppreference:

If the function throws an exception of the type not listed in its exception specification, the function std::unexpected is called. The default function calls std::terminate, but it may be replaced by a user-provided function (via std::set_unexpected) which may call std::terminate or throw an exception. If the exception thrown from std::unexpected is accepted by the exception specification, stack unwinding continues as usual. If it isn't, but std::bad_exception is allowed by the exception specification, std::bad_exception is thrown. Otherwise, std::terminate is called.

Why the feature was removed? Well, it was wonky from the start. Java has identical feature and while not removed, it receives strong criticizm. The reasons for removing dynamic exception specification from C++ were:

  • It did not enforce what it stated at compile time. In the case of Java, compilers can track possible exceptions and block code at compile time that violates it, but in case of C++ the feature was introduced too late to put such a high requirement on already existing code. Thus, std::unexpected was born.

  • There was hope for potential performance gains from providing such information to compilers. No possible optimizations were found. Even worse, the feature has actually prevented certain optimizations because of additional violation checks that had to be performed.

  • Because there were no compile time checks, writing dynamic-exception-specification-correct code was a huge burden on programmers (and still is in Java). The meta of ignorance in both languages reached a point where people simply specified standard library base exception class for every function, making it useless in Java and marginally-informative in C++ (which allows non-class exceptions which no one uses).

  • The feature was not consistent with C++ type system. Unlike const and volatile, it could not fully propagate and was not syntaxically allowed in some subgrammars (e.g. typedefs of function pointers). Exception specification did not participate in function's type, except when it did.

For a longer description read this article by Herb Sutter.

noexcept specification

Exception specification is not gone entirely, but it has been reduced to 2 simplest cases:

  • a function can throw objects of any type (nothing is written)

  • a function can not throw (noexcept is written) (if it actually throws - std::terminate)

And this time it fully works with the type system - analogically to const and volatile the qualification can be increased, but not removed. That is:

  • You can assign addresses of noexcept functions to function pointers, but not vice versa (non-noexcept functions to noexcept function pointers).

  • If a base class virtual function is noexcept, all overrides have to be too.

  • Just like the return type of a function:

    • It is a part of the function type.

    • It is not a part of the function signature - it is not possible to overload functions only by exception specification.

noexcept allows some optimizations on stack unwinding that normally would break implementation of destructors. It also informs the programmer that a given function is safe to use in exception-unsafe contexts such as the body of a destructor or any other function that is also noexcept.

It's possible to call potentially throwing functions and have an exception inside a noexcept function. The exception should just not escape such function (must be caught inside). It if escapes - std::terminate.

noexcept(bool)

The keyword noexcept can accept a constant expression that is convertible to bool. Defaults to true, which means noexcept specifier is equivalent to noexcept(true). This feature is typically used within function templates to conditionally apply noexcept depending on template parameters.

noexcept operator

Apart from exception specification, the keyword can also be used as an operator. It returns a compile time constant of type bool which value is true if the expression can not throw exceptions. Just like sizeof and alignof operators, the expression is an unevaluated context - it is not executed, only checked for specific behavior. The returned value is constexpr but the expression itself does not have to be.

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <vector>

int main()
{
	// If executed, the expression in the first noexcept would result in undefined behavior.
	// But because the noexcept operator has unevaluated context, there is no problem.
	std::cout << std::boolalpha; // print true/false instead of 1/0
	std::cout << "Can vector's operator[] throw? " << !noexcept(std::vector<int>()[0]) << '\n';
	std::cout << "Can vector's at function throw? " << !noexcept(std::vector<int>().at(0)) << '\n';
}

In rare cases both the noexcept specifier and noexcept operator can appear together: T func() noexcept(noexcept(T())). This tests the expression and immediately uses the result to specify whether the current function is noexcept.

Couldn't it be simplified to just 1 use of the keyword?

While it looks like a simplification, it would actually result in a significantly more complicated definition of the keyword - it would have to dynamically adapt depending on a complex context information; some ambiguities might arise. The current definition is much simpler: noexcept works as an operator except in the immediate context of a function (template) declaration - then a specifier. Apparently the committee is fine with this approach, because C++20 introduces identical situation with double requires for concepts.

In practice

Apply noexcept very carefully. Just because a function doesn't throw doesn't mean it will never do after any future edits. You can easily std::terminate your program by applying noexcept and realizing that many stack frames deeper some subfunction can throw. Don't use noexcept unless you can verify and guarantee that all calls within will never throw.

Only a small subset of functions is recommended to be marked as noexcept:

  • special member functions: move constructor and move assignment (destructors are already implicitly noexcept)

  • swap operations (a kind of functions written to implement copy and swap idiom which is used in some special member functions)

  • memory and resource (de)allocation functions (pretty low-level code) - such functions are used within destructors

  • functions intended to operate in an exception-unsafe context:

    • interacting with C: passing addresses of C++ functions to a C API - if C code calls a C++ function and it throws, undefined behavior occurs

    • badly written C++ - there is a lot of exception-unsafe C++ code, including whole libraries (e.g. Qt)

Special member functions do not have to be noexcept, but if they are the C++ standard library takes advantage of it in various places (mostly containers and algorithms - improved performance or better exception guarantees).