C++ noexcept Specifier: Contracts, Moves, and std::terminate

Key takeaways

C++ noexcept explained: exception contracts, conditional noexcept, move constructors, vector behavior, and the noexcept operator.

What is noexcept?

It states that a function does not throw. Crucially, noexcept is a promise the compiler trusts, not one it verifies — nothing stops you from writing throw inside a function marked noexcept, and the compiler generally won’t catch this at compile time (a throw from a genuinely noexcept-incompatible call might trigger a warning, but the general case isn’t checked). What actually happens if that promise is broken is covered in the pitfalls section below: the runtime consequence is std::terminate, not a normal exception the caller can catch. This makes noexcept fundamentally different from a try/catch block — it’s a static contract about the function’s interface, and honoring it is entirely the implementer’s responsibility.

void func() noexcept {
    // must not throw
}
void func2() noexcept(true) {
    // same as noexcept
}
void func3() noexcept(false) {
    // may throw (default for most functions)
}

Benefits of noexcept

The compiler-optimization benefit isn’t hypothetical: when a function can throw, the compiler has to generate exception-handling metadata (unwind tables) and keep the stack in a state that can be unwound cleanly at any point, which can inhibit certain optimizations like aggressive inlining or reordering. A noexcept function tells the compiler none of that machinery is needed for calls into it, simplifying the generated code. The move-constructor case is the one that matters most in everyday code, though, and is significant enough that it gets its own section on vector behavior further down — many standard containers make a real behavioral decision (move vs. copy) based on whether your move constructor is noexcept.

// 1. Compiler optimizations
void swap(int& a, int& b) noexcept {
    int temp = a;
    a = b;
    b = temp;
}
// 2. Move constructors
class MyClass {
public:
    MyClass(MyClass&&) noexcept {
        // containers prefer noexcept moves
    }
};
// 3. Clear contract
int calculate(int x) noexcept {
    return x * 2;  // no exceptions
}

Conditional noexcept

Generic code can’t always claim noexcept unconditionally, because whether the operation actually throws depends on the type T it’s instantiated with — a swap built from moves is only as safe as T’s move constructor. noexcept(expression) lets you compute the specification itself: the inner noexcept(T(std::move(a))) is the noexcept operator, evaluating at compile time to true or false depending on whether constructing a T from an rvalue is itself marked noexcept, and that boolean then becomes the specification for the outer function. The two forms shown are equivalent in effect — std::is_nothrow_move_constructible<T>::value is simply a named, more readable trait for the same underlying query, and is generally preferred in real code because it states the intent directly rather than nesting noexcept inside noexcept.

template<typename T>
void swap(T& a, T& b) noexcept(noexcept(T(std::move(a)))) {
    T temp(std::move(a));
    a = std::move(b);
    b = std::move(temp);
}
// Simpler form
template<typename T>
void swap(T& a, T& b) noexcept(std::is_nothrow_move_constructible<T>::value) {
    // ...
}

noexcept on moves, swap, destructors, and in vector

A noexcept move constructor

Notice the asymmetry between the copy constructor and the move constructor here: the copy constructor allocates new memory with new char[], which can throw std::bad_alloc, so it’s correctly left without a noexcept specifier. The move constructor, by contrast, only steals the other object’s pointer and resets it to nullptr — no allocation, no operation that can fail — so it genuinely can be noexcept, and marking it so is what lets container types like std::vector<String> move-construct String elements during reallocation instead of falling back to the (throwing, and thus strong-exception-safety-guaranteed) copy path.

class String {
private:
    char* data;
    size_t length;
    
public:
    String(const char* str) {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }
    
    ~String() {
        delete[] data;
    }
    
    // Copy ctor (may throw)
    String(const String& other) {
        length = other.length;
        data = new char[length + 1];  // may throw
        strcpy(data, other.data);
    }
    
    // Move ctor (noexcept)
    String(String&& other) noexcept 
        : data(other.data), length(other.length) {
        other.data = nullptr;
        other.length = 0;
    }
    
    // Move assignment (noexcept)
    String& operator=(String&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            length = other.length;
            other.data = nullptr;
            other.length = 0;
        }
        return *this;
    }
};

A noexcept swap

The free-function friend void swap(MyClass&, MyClass&) noexcept here is the classic “swap idiom” used throughout the standard library and copy-and-swap assignment implementations: swapping just the internal pointers can never fail, since it only reassigns already-allocated members without touching the heap, which is exactly the property noexcept should be reserved for. Declaring swap as a friend found via argument-dependent lookup (rather than a member function) also lets generic code call unqualified swap(a, b) and have it resolve to this specialized version instead of the generic std::swap, which is why the friend swap below writes using std::swap; before calling unqualified swap(a.data, b.data) — the same two-step makes ADL-found overloads win for member types that have them, with std::swap as the fallback.

#include <utility>

class MyClass {
private:
    int* data;
    
public:
    MyClass(int value) : data(new int(value)) {}
    
    ~MyClass() {
        delete data;
    }
    
    friend void swap(MyClass& a, MyClass& b) noexcept {
        using std::swap;
        swap(a.data, b.data);
    }
};

Destructors are already noexcept

Writing noexcept on this destructor is actually redundant — since C++11, destructors are implicitly noexcept(true) by default unless the class has a base or member whose destructor is explicitly noexcept(false), or you override that default yourself. The comment is worth internalizing regardless: because destructors run during stack unwinding (while another exception is already propagating), a destructor that throws while unwinding is already in std::terminate territory per the language rules, which is why the standard library and idiomatic C++ treat “destructors don’t throw” as close to a hard rule rather than a style preference.

class Resource {
private:
    int* data;
    
public:
    Resource(int value) : data(new int(value)) {}
    
    // destructor is noexcept by default
    ~Resource() noexcept {
        delete data;
        // must not throw from destructor
    }
};

Counting copies during vector growth

The easiest way to see the effect is to count. The two types below are identical except for the noexcept on the move constructor; both are copyable:

#include <cstdio>
#include <vector>

struct Tracer {
    static inline int copies = 0, moves = 0;
    Tracer() = default;
    Tracer(const Tracer&) { ++copies; }
    Tracer(Tracer&&) { ++moves; }            // not noexcept
};

struct TracerNx {
    static inline int copies = 0, moves = 0;
    TracerNx() = default;
    TracerNx(const TracerNx&) { ++copies; }
    TracerNx(TracerNx&&) noexcept { ++moves; }
};

int main() {
    std::vector<Tracer> a;
    std::vector<TracerNx> b;
    for (int i = 0; i < 8; ++i) { a.emplace_back(); b.emplace_back(); }
    std::printf("Tracer:   copies=%d moves=%d\n", Tracer::copies, Tracer::moves);
    std::printf("TracerNx: copies=%d moves=%d\n", TracerNx::copies, TracerNx::moves);
}

Built with g++ 10 (-std=c++17), where capacity grows 1 → 2 → 4 → 8, this prints:

Tracer:   copies=7 moves=0
TracerNx: copies=0 moves=7

Every one of the seven element relocations during growth was a copy for Tracer and a move for TracerNx. The emplace_back calls themselves construct in place and aren’t counted. Nothing in Tracer’s move constructor can throw either — the compiler doesn’t look at the body, only at the declaration.

std::vector needs this distinction because of its strong exception guarantee during reallocation: if growing the vector’s buffer fails partway through, the vector must be able to roll back to its exact previous state, as if push_back had never been called. A move that could throw partway through would leave some elements moved-out in the old buffer and some still there, with no way to safely reconstruct the original vector — so vector conservatively falls back to copying (which it can safely roll back from, since the originals are untouched) unless it can verify at compile time, via std::is_nothrow_move_constructible, that moving is safe to commit to unconditionally. The decision is expressed by std::move_if_noexcept, which libstdc++ and libc++ use when relocating elements. Its exact rule has a twist that surprises people: it returns an rvalue (so the element is moved) if the move constructor is noexcept or if the type isn’t copyable at all. It only falls back to copying when the move might throw and a copy constructor exists:

Move constructorCopy constructorWhat reallocation doesStrong guarantee kept?
noexceptanyMovesYes
may throwavailableCopiesYes
may throwdeleted (move-only type)Moves anywayNo — if a move throws midway, the vector is left in a valid but unspecified state

So a move-only type such as a class holding a std::unique_ptr and a user-written move constructor without noexcept doesn’t get slower — it silently loses the strong exception guarantee instead. One more trap: declaring only a move constructor implicitly deletes the copy constructor, so a class written “just to test” the no-noexcept case is move-only and will be moved regardless. That’s why both tracer types above spell out a copy constructor.

The noexcept operator

Don’t confuse the noexcept specifier (which appears after a function’s parameter list, void f() noexcept) with the noexcept operator (noexcept(expr)), which is a compile-time query — it yields false if any part of the expression is potentially throwing (a call to a non-noexcept function, a throw, a dynamic_cast to a reference, and so on), and it does so without evaluating the expression (much like sizeof). noexcept(f(), g()) is false if either call may throw. This is why noexcept(throw 1) evaluates to false rather than actually throwing: throw expressions are, by definition, never noexcept, and the operator only checks that fact at compile time; the throw inside never executes.

#include <iostream>
// noexcept(expr): is expr noexcept?
void func1() noexcept {}
void func2() {}
int main() {
    std::cout << noexcept(func1()) << std::endl;  // 1 (true)
    std::cout << noexcept(func2()) << std::endl;  // 0 (false)
    
    std::cout << noexcept(1 + 2) << std::endl;    // 1
    std::cout << noexcept(throw 1) << std::endl;  // 0
}

Terminate calls, over-promising templates, and forgotten moves

Throwing from a noexcept function

The try/catch in main doesn’t help here, and that’s the whole point of the pitfall: when a noexcept function throws, the runtime calls std::terminate before stack unwinding reaches any caller’s catch block — it doesn’t even attempt to find a matching handler further up the call stack. This is different from an uncaught exception in ordinary code, which does unwind the stack looking for a handler and only calls terminate if none exists; violating noexcept short-circuits that search entirely. In practice this usually surfaces as the program aborting with a message like “terminate called after throwing an instance of…” and no user-visible exception handling ever running.

void func() noexcept {
    throw std::runtime_error("error");  // std::terminate!
}
int main() {
    try {
        func();
    } catch (...) {
        // may not run: terminate first
    }
}

Unconditional noexcept on a template

Marking a template function unconditionally noexcept is a trap precisely because the template is instantiated for many different Ts, and the author of process has no way to know in advance whether every future T’s copy constructor can throw. If some T genuinely does throw during T copy = value;, that throw happens inside a function promised as noexcept, triggering std::terminate — a crash caused not by a bug in process itself, but by a type parameter the template’s author never anticipated. Tying the specification to is_nothrow_copy_constructible<T> makes the guarantee automatically correct for every T, rather than a blanket claim that’s only true for some of them.

// Wrong: always noexcept
template<typename T>
void process(T value) noexcept {
    T copy = value;  // copy may throw
}
// Better
template<typename T>
void process(T value) noexcept(std::is_nothrow_copy_constructible<T>::value) {
    T copy = value;
}

A destructor that throws

BadClass’s destructor compiles today, but it’s a latent bug waiting for the day this object is destroyed during stack unwinding from another exception — at that point, the language has two simultaneously-propagating exceptions and no well-defined way to handle both, so the runtime calls std::terminate immediately, regardless of whether the destructor’s own exception would otherwise have been catchable. GoodClass sidesteps the whole problem by catching everything inside the destructor and never letting an exception escape it — swallowing or logging it is the pragmatic choice, since there’s no caller left to hand the exception to by the time a destructor runs implicitly (e.g., via RAII or going out of scope).

// Bad
class BadClass {
public:
    ~BadClass() {
        throw std::runtime_error("error");  // dangerous
    }
};
// Good
class GoodClass {
public:
    ~GoodClass() noexcept {
        try {
            // risky code
        } catch (...) {
            // swallow or log
        }
    }
};

A move constructor without noexcept

This is the same vector reallocation behavior from the vector growth example stated as a pitfall in its own right, because it’s easy to write a perfectly correct, non-throwing move constructor and simply forget the noexcept keyword — the code behaves identically either way for direct calls, so nothing looks wrong, but vector will silently fall back to the slower copy path during growth since it can’t verify at compile time that the move is safe. This class of bug is invisible in normal testing and only shows up as an unexplained performance regression under profiling, which is why it’s worth defaulting to noexcept on move operations whenever they genuinely can’t throw, rather than only adding it reactively.

// Without noexcept: vector copies on growth (because a copy ctor exists)
class MyClass {
public:
    MyClass(const MyClass& other);
    MyClass(MyClass&& other) {
        // ...
    }
};
// With noexcept: vector moves on growth
class MyClass {
public:
    MyClass(const MyClass& other);
    MyClass(MyClass&& other) noexcept {
        // ...
    }
};

The most common way to hit this without writing a move constructor at all is adding a destructor. A user-declared destructor (even an empty one added for logging) suppresses the implicitly generated move operations, so std::move on the object quietly calls the copy constructor instead, and std::is_nothrow_move_constructible turns false if any member’s copy can throw. The cheapest insurance is a static_assert next to the class definition, shown in the production patterns below.

noexcept and performance

Again, only one of these move constructors would actually be declared in a real class — they’re shown together here to contrast the two paths a benchmark would take.

#include <vector>
// Without noexcept: every reallocation copies all existing elements
class WidgetCopying {
    int* data;
    
public:
    WidgetCopying(int value) : data(new int(value)) {}
    
    ~WidgetCopying() {
        delete data;
    }
    
    WidgetCopying(const WidgetCopying& other)   // deep copy: allocates
        : data(new int(*other.data)) {}
    
    WidgetCopying(WidgetCopying&& other) 
        : data(other.data) {
        other.data = nullptr;
    }
};
// With noexcept: every reallocation moves existing elements
class WidgetMoving {
    int* data;
    
public:
    WidgetMoving(int value) : data(new int(value)) {}
    
    ~WidgetMoving() {
        delete data;
    }
    
    WidgetMoving(const WidgetMoving& other)
        : data(new int(*other.data)) {}
    
    WidgetMoving(WidgetMoving&& other) noexcept 
        : data(other.data) {
        other.data = nullptr;
    }
};
int main() {
    std::vector<WidgetMoving> vec;
    
    // noexcept move -> fast path on growth
    for (int i = 0; i < 1000000; i++) {
        vec.push_back(WidgetMoving(i));
    }
}

The performance gap compounds with vector size: each reallocation touches every existing element, so with the copying version, growing a million-element vector re-copies (and re-heap-allocates via each element’s own constructor) elements that were already fully constructed, over and over across each doubling of capacity. With push_back’s amortized-O(1) growth strategy, the total number of element operations across all reallocations is still linear in the final size either way, but a copy is inherently more expensive per element than a pointer-stealing move — for a type like Widget that owns a heap allocation, the difference between “reassign a pointer” and “allocate new memory, copy the value, free the old memory” is the entire cost gap this section is illustrating.

Where noexcept belongs

The common thread across the “good” candidates is that each operation’s failure mode is structurally impossible, not just unlikely — a move that only reassigns pointers, a swap that only exchanges members, a getter that only returns a stored value. The “avoid” list is really one principle: don’t claim a guarantee you can’t keep just because it seems convenient today, since a broken promise here doesn’t produce a normal exception you can debug with a stack trace — it produces an immediate std::terminate, which is a much worse failure mode than the exception you were trying to avoid.

// ✅ Good noexcept candidates
// 1. Move ctor / move assign
MyClass(MyClass&&) noexcept;
MyClass& operator=(MyClass&&) noexcept;
// 2. swap
void swap(MyClass&, MyClass&) noexcept;
// 3. Destructor (implicitly noexcept)
~MyClass() noexcept;
// 4. Simple getters
int getValue() const noexcept;
// ❌ Avoid noexcept when
// 1. Function may throw
void process() {
    // may throw
}
// 2. Future may add throws
void complexOperation() {
    // evolving API
}

FAQ

Q1: When to use noexcept?

A:

  • Move operations
  • swap
  • Destructors
  • Functions that truly never throw

Q2: What if you violate noexcept?

A: std::terminate—the program ends immediately, without unwinding the stack to look for a matching catch.

Q3: Performance benefits?

A:

  • Fewer unwind paths to generate
  • vector and other containers can move elements during reallocation instead of copying
  • Stronger optimization hints for the compiler

Q4: Conditional noexcept?

A: noexcept(expression)—essential in templates, where whether an operation can throw depends on the type parameter.

Q5: Destructors?

A: Implicitly noexcept since C++11 unless a base or member overrides that; do not throw from them.