The noexcept specifier, introduced in C++11, promises that a function will not throw an exception. If a noexcept function throws an exception, std::terminate is called immediately.

The compiler can leverage this information to perform specific optimizations on non-throwing functions and enable the noexcept operator, which checks at compile-time if a particular expression is declared not to throw exceptions.

Before C++11, the dynamic exception specifier throw() was used, but it was deprecated in C++11 and removed entirely in C++17.

Example:

void runNoexcept() noexcept {    
}

// `noexcept` is equivalent to `noexcept(true)`
void runNoexcept2() noexcept(true) {    
}

void throwInvalidArgument() noexcept {
    throw std::invalid_argument("invalid argument");     
}

int main() 
{
    runNoexcept();  // fine
    runNoexcept2();  // fine
    throwInvalidArgument();  // compiles, but at runtime calls std::terminate
}

The main benefits of using noexcept are:

  1. noexcept functions are more optimizable than non-noexcept functions.
  2. It is particularly valuable for move operations, swap functions, memory deallocation, and destructors.

Move Semantics and noexcept

During the development of move semantics in C++11, a problem arose: vector reallocations could not safely use move semantics without the noexcept guarantee. This led to the introduction of the noexcept specifier.

Below is an example illustrating the significance of noexcept in the context of move semantics:

#include <string>
#include <iostream>
#include <vector>

class Artist
{
private:
    std::string name;

public:
    Artist(const char* n) : name{n} {}

    std::string getName() const
    {
        return name;
    }

    // Prints a message when copying or moving:
    Artist(const Artist& a) : name{a.name}
    {
        std::cout << "COPY " << name << '\n';
    }

    Artist(Artist&& a) // if noexcept, guarantees not to throw
        : name{std::move(a.name)}
    {
        std::cout << "MOVE " << name << '\n';
    }
    // ...
};

int main()
{
    // Step 1
    std::vector<Artist> artists{"Pink Floyd", "Frank Zappa", "Daft Punk"};
    std::cout << "capacity: " << artists.capacity() << '\n';

    // Step 2
    artists.push_back("Kendrick Lamar");
}

Output:

COPY Pink Floyd
COPY Frank Zappa
COPY Daft Punk
capacity: 3
MOVE Kendrick Lamar
COPY Pink Floyd
COPY Frank Zappa
COPY Daft Punk

Steps explantation:

  1. Initialization: The initial elements are copied into the vector, which typically allocates memory for three elements.
  2. Reallocation: When the fourth element is inserted using push_back(), the vector reallocates more memory. It moves the new element, but it copies the existing elements to the new memory location. Afterward, the old elements are destroyed, and their memory is deallocated.

The reason the existing elements are copied instead of moved is due to the strong exception safety guarantee:

If an exception is thrown during the reallocation of a vector, the C++ standard library guarantees that the vector will be rolled back to its previous state.

In this example, during Step 2, the container cannot move the initial elements because if an exception were thrown during the move operation, it would be impossible to restore the previous state.

Applying the noexcept specifier to the move constructor satisfies the strong exception safety guarantee, as the compiler now knows that the move operation will not throw exceptions. Consequently, the container can move the initial elements during reallocation.

// ...
Artist(Artist&& a) noexcept // guarantees not to throw
    : name{std::move(a.name)}
{
    std::cout << "MOVE " << name << '\n';
}
// ...

After recompiling, the output becomes:

COPY Pink Floyd
COPY Frank Zappa
COPY Daft Punk
capacity: 3
MOVE Kendrick Lamar
MOVE Pink Floyd
MOVE Frank Zappa
MOVE Daft Punk 

Conditional noexcept

What assures us that the move constructor's operations will not throw exceptions? According to [2], the operations involved are:

  1. Moving the name (a std::string).
  2. Writing to the standard output stream.

If any of these operations throw exceptions, it would violate the noexcept promise, causing std::terminate() to be called. Therefore, to safely mark the move constructor as noexcept, we must ensure that neither the string move nor the output operation will throw.

The noexcept specifier allows us to guarantee no exceptions conditionally. This would look like:

// ...
Artist(Artist&& a) noexcept(std::is_nothrow_move_constructible_v<std::string> 
                            && noexcept(std::cout << name))
    : name{std::move(a.name)}
{
    std::cout << "MOVE " << name << '\n';
}
// ...

Where:

  • std::is_nothrow_move_constructible_v<std::string> is a metafunction that ensures the std::string move constructor does not throw.
  • noexcept(std::cout << name) checks whether the output expression guarantees not to throw.

References


Published

Category

Implementation details

Tags

Contact