Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 3 of 5

noexcept on a function says: this will not throw. It is not a request or a hint. If the function throws anyway, the program is terminated on the spot.

That severity is the point. Because the promise is absolute, the standard library can make decisions based on it — and the difference is measurable.

The one that matters: vector reallocation

#include <iostream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>

// Two types identical except for one keyword.
struct MoveThrows {
    std::string data;
    static int moves, copies;

    explicit MoveThrows(std::string d) : data(std::move(d)) {}
    MoveThrows(const MoveThrows& o) : data(o.data) { ++copies; }
    MoveThrows(MoveThrows&& o) : data(std::move(o.data)) { ++moves; }   // NOT noexcept
    MoveThrows& operator=(const MoveThrows&) = default;
    MoveThrows& operator=(MoveThrows&&) = default;
};

struct MoveNoexcept {
    std::string data;
    static int moves, copies;

    explicit MoveNoexcept(std::string d) : data(std::move(d)) {}
    MoveNoexcept(const MoveNoexcept& o) : data(o.data) { ++copies; }
    MoveNoexcept(MoveNoexcept&& o) noexcept : data(std::move(o.data)) { ++moves; }
    MoveNoexcept& operator=(const MoveNoexcept&) = default;
    MoveNoexcept& operator=(MoveNoexcept&&) = default;
};

int MoveThrows::moves = 0;
int MoveThrows::copies = 0;
int MoveNoexcept::moves = 0;
int MoveNoexcept::copies = 0;

template <typename T>
void fill_and_grow(const char* label, int& moves, int& copies) {
    moves = 0;
    copies = 0;
    std::vector<T> v;
    for (int i = 0; i < 8; ++i) {
        v.push_back(T("item " + std::to_string(i)));
    }
    std::cout << "  " << label << ": " << moves << " moves, " << copies << " copies\n";
}

int main() {
    std::cout << std::boolalpha;
    std::cout << "is_nothrow_move_constructible:\n";
    std::cout << "  MoveThrows:   " << std::is_nothrow_move_constructible_v<MoveThrows> << "\n";
    std::cout << "  MoveNoexcept: " << std::is_nothrow_move_constructible_v<MoveNoexcept> << "\n";

    std::cout << "\nreallocating a vector to 8 elements:\n";
    fill_and_grow<MoveThrows>("throwing move ", MoveThrows::moves, MoveThrows::copies);
    fill_and_grow<MoveNoexcept>("noexcept move", MoveNoexcept::moves, MoveNoexcept::copies);
    return 0;
}
is_nothrow_move_constructible:
  MoveThrows:   false
  MoveNoexcept: true

reallocating a vector to 8 elements:
  throwing move : 8 moves, 7 copies
  noexcept move: 15 moves, 0 copies

Two types identical except for one keyword, and the results differ completely.

The type with a throwing move made 7 copies. The type with a noexcept move made 0.

The reason is lesson 15.2. std::vector::push_back offers the strong guarantee: if it throws, nothing changed. During a reallocation it must relocate existing elements into the new buffer — and if a move threw halfway through, the old elements would already be gutted and the new buffer incomplete. There would be no way back.

So vector asks: is this type's move constructor noexcept?

  • Yes → move the elements. Fast, and it cannot fail.
  • No → copy them. Slow, but the originals survive, so a throw leaves the vector untouched.

This is std::move_if_noexcept, and it is why a move constructor without noexcept silently costs you the move semantics you thought you had written (lesson 9.9).

Mark your move constructor and move assignment noexcept. It is the single highest-value use of the keyword.

The counts are worth reading precisely. The noexcept version made 15 moves: 8 moving each temporary into the vector, plus 7 relocating during the three reallocations (1 + 2 + 4). The throwing version made 8 moves — only the temporaries, since those are separate from reallocation — and 7 copies where the other moved.

What happens when you lie

// CRASHES ON PURPOSE — a noexcept function that throws calls std::terminate.
#include <iostream>
#include <stdexcept>

void lies() noexcept {
    throw std::runtime_error("this never reaches a handler");
}

int main() {
    std::cout << "before\n";
    try {
        lies();
    } catch (const std::exception& e) {
        std::cout << "this handler is never reached: " << e.what() << "\n";
    }
    std::cout << "after\n";
    return 0;
}

GCC sees it coming:

warning: 'throw' will always call 'terminate' [-Wterminate]

And running it confirms:

before
terminate called after throwing an instance of 'std::runtime_error'
  what():  this never reaches a handler
Aborted

Exit code 134. The catch block never ran. A noexcept violation does not propagate — it calls std::terminate immediately, and no handler anywhere in the program gets a chance.

That is why noexcept is a promise rather than a wish: the compiler stops generating unwinding code for the function, so there is no mechanism left to carry an exception out of it.

The noexcept operator

Confusingly, the same keyword is also a compile-time question:

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

struct Simple { int a, b; };
struct HasString { std::string s; };
struct Throwing {
    Throwing() {}
    Throwing(Throwing&&) { }            // not noexcept
};

// Conditional noexcept: this function is noexcept only if T's move is.
template <typename T>
void relocate(T& dest, T& src) noexcept(std::is_nothrow_move_assignable_v<T>) {
    dest = std::move(src);
}

int main() {
    std::cout << std::boolalpha;

    // The noexcept OPERATOR asks whether an expression can throw.
    int x = 0;
    std::cout << "noexcept(x + 1):            " << noexcept(x + 1) << "\n";
    std::cout << "noexcept(new int):          " << noexcept(new int) << "\n";
    std::cout << "noexcept(std::string(\"a\")): " << noexcept(std::string("a")) << "\n";

    std::cout << "\ncompiler-generated moves:\n";
    std::cout << "  Simple:    " << std::is_nothrow_move_constructible_v<Simple> << "\n";
    std::cout << "  HasString: " << std::is_nothrow_move_constructible_v<HasString> << "\n";
    std::cout << "  Throwing:  " << std::is_nothrow_move_constructible_v<Throwing> << "\n";

    std::cout << "\nconditional noexcept on relocate:\n";
    Simple s1{1, 2}, s2{3, 4};
    HasString h1, h2;
    std::cout << "  relocate<Simple>:    " << noexcept(relocate(s1, s2)) << "\n";
    std::cout << "  relocate<HasString>: " << noexcept(relocate(h1, h2)) << "\n";

    // Destructors are implicitly noexcept.
    std::cout << "\nstd::string destructor is noexcept: "
              << std::is_nothrow_destructible_v<std::string> << "\n";
    return 0;
}
noexcept(x + 1):            true
noexcept(new int):          false
noexcept(std::string("a")): false

compiler-generated moves:
  Simple:    true
  HasString: true
  Throwing:  false

conditional noexcept on relocate:
  relocate<Simple>:    true
  relocate<HasString>: true

std::string destructor is noexcept: true

Three distinct uses of one keyword:

// void f() noexcept;                            // the SPECIFIER: a promise
// bool b = noexcept(expr);                      // the OPERATOR: a question
// void g() noexcept(condition);                 // CONDITIONAL: promise if condition

noexcept(new int) is false because allocation can throw std::bad_alloc. Almost anything that allocates is potentially throwing, which is why std::string("a") is too.

The compiler generates noexcept moves when it can. Simple and HasString both get noexcept move constructors for free, because moving an int or a std::string cannot throw. Write a move constructor by hand and you lose that unless you say so — which is the Throwing row, and the single most common way to lose move semantics accidentally.

Conditional noexcept is how generic code stays honest. relocate<T> promises not to throw exactly when T's move assignment does not. Hardcoding noexcept there would be a lie for some T; omitting it would be a needless pessimisation for others.

Where to put it

Always:

  • Move constructor and move assignment. The measured reason above.
  • swap. Copy-and-swap (lesson 15.2) depends on swap being the commit point that cannot fail.
  • Destructors. They are already implicitly noexcept — see below.

Usually:

  • Simple accessors: size(), empty(), data(), operator[].
  • Anything that only touches built-in types.

Do not:

  • On a function that allocates, or calls one that might.
  • On anything whose implementation you expect to change. Removing noexcept later is a breaking change for callers who relied on it — including the standard library.
  • "Just in case." An unmarked function is not a problem; a wrongly marked one terminates the process.

Destructors are already noexcept

Since C++11, a destructor is implicitly noexcept unless you explicitly say otherwise. The output above confirms it for std::string.

This is not a convenience — it is load-bearing. Recall lesson 15.1: when an exception propagates, the stack unwinds and destructors run. If a destructor threw during that unwinding, there would be two exceptions in flight at once, and the language has no answer for that. So it terminates.

The practical rule: never let an exception escape a destructor. If a destructor must do something that can fail — flushing a buffer, closing a socket — catch and log it:

// ~Logger() {
//     try {
//         flush();
//     } catch (...) {
//         // Nothing else to do. Terminating is worse than a lost log line.
//     }
// }

That is one of the few legitimate uses of a silent catch (...).

Try it

Take a resource-owning class from Module 9 or 11 and measure the effect of noexcept on its move constructor: count allocations and copies while filling a std::vector with 16 of them, with and without the keyword. Then add a conditional noexcept to a generic wrapper.

Checkpoint

  • noexcept is a promise, not a hint. Breaking it calls std::terminate immediately.
  • A noexcept violation does not propagate — no handler anywhere gets a chance.
  • Mark move constructors and move assignment noexcept. Measured: 7 copies became 0.
  • std::vector copies instead of moving during reallocation when the move can throw, to preserve the strong guarantee.
  • Writing a move constructor by hand loses the implicit noexcept unless you restate it.
  • noexcept(expr) is also an operator asking whether an expression can throw.
  • noexcept(condition) is conditional — the only honest choice in generic code.
  • Every standard-library move is conditionally noexcept for exactly that reason.
  • Mark swap noexcept; copy-and-swap needs a commit point that cannot fail.
  • Destructors are implicitly noexcept; a throwing destructor during unwinding terminates.
  • Never let an exception escape a destructor — catch and log instead.
  • Removing noexcept later is a breaking change; do not add it speculatively.

Questions about this lesson

Log in to ask a question.