Skip to content
C++
Language
since C++11
Intermediate

noexcept — Exception Specifications

"noexcept specifier and operator: marking functions non-throwing, conditional noexcept, move semantics optimization, and type-system implications since C++17."

noexceptsince C++11

noexcept is a function specifier that promises not to propagate exceptions, and a compile-time operator that queries whether an expression is non-throwing — the combination enables containers and algorithms to select move-or-copy strategies at compile time.

Overview

Before C++11, dynamic exception specifications (throw(T, U)) required runtime infrastructure to unwind the stack before calling std::terminate on violation, and the compiler could not optimize around them. C++11 replaced them with noexcept. A noexcept violation calls std::terminate immediately with no unwinding guarantee, which both simplifies the ABI and enables more aggressive optimizer decisions on code paths.

The effect on the standard library is concrete: std::vector internally uses std::move_if_noexcept during reallocation. If a type's move constructor is not noexcept, the container falls back to copying every element to preserve the strong exception guarantee. A move constructor that isn't marked noexcept silently degrades every push_back that triggers growth from O(1) moves to O(n) copies. No compiler warning, no diagnostic — just invisible regression.

Since C++17, noexcept is part of the function's type, affecting function pointer conversions, virtual function overrides, and template instantiation.

Dynamic exception specifications (including throw()) were deprecated in C++11 and removed in C++17. There is no replacement for throw(TypeList) — use noexcept for the nothrow guarantee or leave the function potentially-throwing.

Syntax

Specifier forms:

cpp
void f() noexcept;              // non-throwing — C++11
void g() noexcept(true);        // equivalent to noexcept
void h() noexcept(false);       // potentially throwing — same as no specifier

Conditional noexcept — the argument must be a constant expression evaluating to bool:

cpp
template<typename T>
void move_assign(T& dst, T& src)
    noexcept(std::is_nothrow_move_assignable_v<T>) {  // _v alias: C++17
    dst = std::move(src);
}

noexcept operator — compile-time query, the expression is never evaluated:

cpp
static_assert( noexcept(1 + 2) );
static_assert(!noexcept(std::vector<int>{}.push_back(0)) );

Double-noexcept idiom — propagates noexcept-ness from sub-operations without duplicating the condition:

cpp
template<typename T>
void push(T&& val)
    noexcept(noexcept(data_.push_back(std::forward<T>(val)))) {
    data_.push_back(std::forward<T>(val));
}

The outer noexcept(...) is the specifier; the inner noexcept(...) is the operator. Together they ask: "is this particular call noexcept?" and use the answer as the function's own guarantee.

Examples

Move operations and container reallocation

cpp
class Buffer {
    std::byte* data_;
    std::size_t size_;
public:
    // noexcept is essential — std::vector checks this before choosing move vs copy
    Buffer(Buffer&& other) noexcept
        : data_(std::exchange(other.data_, nullptr))  // std::exchange: C++14
        , size_(std::exchange(other.size_, 0)) {}

    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = std::exchange(other.data_, nullptr);
            size_ = std::exchange(other.size_, 0);
        }
        return *this;
    }

    ~Buffer() noexcept { delete[] data_; }  // implicitly noexcept in C++11+
};

// Verify at compile time — catches the mistake before it silently degrades perf
static_assert(std::is_nothrow_move_constructible_v<Buffer>);  // C++17 _v alias
static_assert(std::is_nothrow_move_assignable_v<Buffer>);

std::vector<Buffer> vec;
vec.reserve(4);
vec.push_back(Buffer{});  // reallocation moves, not copies

std::move_if_noexcept

std::move_if_noexcept (C++11) returns T&& if the move constructor is noexcept, and const T& otherwise. Use it when implementing containers that must preserve the strong exception guarantee during element relocation:

cpp
template<typename T>
void safe_relocate(T* src, T* dst, std::size_t n) {
    for (std::size_t i = 0; i < n; ++i)
        ::new(dst + i) T(std::move_if_noexcept(src[i]));
    // If T's move ctor is noexcept → moves. Otherwise → copies.
    // If an exception escapes mid-loop, previously constructed elements remain valid.
}

This is exactly what std::vector::push_back does internally when it outgrows its capacity.

noexcept-aware swap

std::swap is conditionally noexcept since C++11. Provide an ADL-visible friend swap so generic algorithms can swap your type without copying:

cpp
class Widget {
    int id_;
    std::string name_;
    std::vector<int> data_;
public:
    friend void swap(Widget& a, Widget& b) noexcept {
        using std::swap;
        swap(a.id_,   b.id_);    // noexcept
        swap(a.name_, b.name_);  // std::string::swap is noexcept (C++11)
        swap(a.data_, b.data_);  // std::vector::swap is noexcept (C++11)
    }
};

static_assert(std::is_nothrow_swappable_v<Widget>);  // C++17 trait

The copy-and-swap assignment idiom depends on this: a noexcept swap gives strong exception guarantee to operator= at zero extra cost.

noexcept and the type system (C++17)

Before C++17, noexcept only appeared in the exception specification — two functions with identical signatures differing only in noexcept were considered the same type. C++17 made it part of the type:

cpp
using NE  = void(*)() noexcept;
using Any = void(*)();

void ne_fn() noexcept {}
void th_fn() {}

NE  p1 = ne_fn;   // OK
Any p2 = ne_fn;   // OK — noexcept→potentially-throwing is an implicit conversion (C++17)
// NE p3 = th_fn; // error: potentially-throwing cannot implicitly convert to noexcept

// Overloading on noexcept alone is not permitted:
// void f() noexcept;  // error: redeclaration
// void f();

std::function does not preserve noexcept in C++17 or C++20 — wrapping a noexcept callable in std::function<void()> silently drops the guarantee. If the no-throw property must survive erasure, use a function pointer, a template parameter, or std::move_only_function (C++23).

noexcept on lambdas

cpp
auto square = [](double x) noexcept -> double { return x * x; };  // C++11
static_assert(noexcept(square(2.0)));

// Propagated noexcept in a generic forwarding lambda (C++14 generic lambda):
auto fwd_invoke = [](auto&& fn, auto&&... args)
    noexcept(noexcept(fn(std::forward<decltype(args)>(args)...)))
    -> decltype(auto) {
    return fn(std::forward<decltype(args)>(args)...);
};

is_nothrow_* type traits

The <type_traits> header provides compile-time queries for all special operations. The traits themselves are C++11; the _v variable template aliases are C++17:

cpp
// trait::value form available in C++11; _v alias requires C++17
std::is_nothrow_default_constructible_v<T>   // T() noexcept?
std::is_nothrow_copy_constructible_v<T>      // T(const T&) noexcept?
std::is_nothrow_move_constructible_v<T>      // T(T&&) noexcept?
std::is_nothrow_copy_assignable_v<T>         // op=(const T&) noexcept?
std::is_nothrow_move_assignable_v<T>         // op=(T&&) noexcept?
std::is_nothrow_destructible_v<T>            // ~T() noexcept?
std::is_nothrow_swappable_v<T>               // swap(T&,T&) noexcept?  (C++17 trait)
std::is_nothrow_invocable_v<F, Args...>      // F(Args...) noexcept?   (C++17 trait)

These are the building blocks for conditional noexcept in generic code. Query properties of T, derive your function's noexcept-ness from them.

Best Practices

Mark all move constructors and move assignment operators noexcept when they only transfer ownership (pointer or handle swaps, std::exchange, std::move on noexcept-movable members). This is the single highest-impact use. Verify with static_assert(std::is_nothrow_move_constructible_v<T>) in the class body — it catches the mistake at the definition site, not at a profiler six months later.

Use conditional noexcept in generic code rather than unconditional noexcept. An unconditional noexcept on a template that calls T operations can silently call std::terminate when instantiated with a throwing T:

cpp
// BAD: T::process() might throw → std::terminate with no useful context
template<typename T>
void process(T& obj) noexcept { obj.process(); }

// GOOD: propagates noexcept-ness from T
template<typename T>
void process(T& obj) noexcept(noexcept(obj.process())) { obj.process(); }

Prefer is_nothrow_* traits over inline noexcept(expr) operator when the condition is about a single named operation — the trait is more readable, handles access control edge cases, and is easier to compose:

cpp
template<typename T>
T extract(std::vector<T>& v)
    noexcept(std::is_nothrow_move_constructible_v<T>) {
    T val = std::move(v.back());
    v.pop_back();
    return val;
}

Catch internally when adapting potentially-throwing code under a noexcept boundary — the right pattern for wrappers over legacy or third-party code you cannot change:

cpp
void safe_flush() noexcept {
    try {
        legacy_stream_.flush();
    } catch (...) {
        // log to a side channel that cannot throw
    }
}

Common Pitfalls

Forgetting noexcept on move operations — no warning, silent perf regression. The only mitigation is static_assert in the class definition. Add it whenever you write a move constructor.

Unconditional noexcept on forwarding wrappers. This is the most dangerous mistake. The wrapper compiles and runs correctly until T throws, at which point std::terminate fires with a call stack that points into your wrapper rather than the actual throwing site.

noexcept does not prevent exceptions inside the function body. A noexcept function can contain try/catch blocks. The specifier only applies at the boundary: an exception that would escape the function calls std::terminate; exceptions caught locally are fine. This also means std::terminate can be called after partial local stack unwinding — not at the throw site.

Virtual overrides cannot be more permissive than the base. Since C++11, an override may not have a looser exception specification than the overridden function:

cpp
struct Base {
    virtual void work() noexcept;
};
struct Derived : Base {
    void work() override;  // error — looser than Base::work() noexcept
};

The reverse is fine — a noexcept override of a potentially-throwing base is allowed.

Destructors are implicitly noexcept in C++11+ but members are not exempt. A destructor that calls a member function that throws will call std::terminate. If a member's destructor is not noexcept (rare but possible with older C code wrapped in RAII), the containing class's destructor is no longer implicitly noexcept.

noexcept and std::vector Reallocation

std::vector::push_back applies the "noexcept or copy" rule during reallocation. Internally the standard library calls std::move_if_noexcept on each existing element: if the element type's move constructor is noexcept, it moves; otherwise it copies every element to preserve the strong exception guarantee. The result is that a missing noexcept on a move constructor silently turns O(1) amortised moves into O(n) copies on every capacity-doubling event.

cpp
// C++11
struct Cheap {
    std::vector<int> data;

    // noexcept move — vector will MOVE elements on reallocation
    Cheap(Cheap&&) noexcept = default;
    Cheap& operator=(Cheap&&) noexcept = default;
};

struct Expensive {
    std::vector<int> data;

    // Missing noexcept — vector falls back to COPYING on reallocation
    Expensive(Expensive&&) = default;
    Expensive& operator=(Expensive&&) = default;
};

// Verify at compile time — catches the regression at the definition site
static_assert(std::is_nothrow_move_constructible_v<Cheap>,
              "Cheap must be noexcept-movable for O(1) vector growth");

// std::is_nothrow_move_constructible_v<Expensive> == false
// Every push_back that triggers growth: O(n) copies, not O(1) moves

The static_assert is the correct mitigation — there is no compiler warning for a missing noexcept on a move constructor, so the assert must be placed deliberately in the class definition. A micro-benchmark that pushes N elements into a std::vector will show the difference clearly: for any non-trivial element type, removing noexcept from the move constructor makes total construction time grow from O(n) to O(n log n).

Conditional noexcept

Conditional noexcept propagates the exception guarantee transitively through templates. The form noexcept(noexcept(expr)) evaluates the inner noexcept operator at compile time (without executing expr) and uses the resulting bool as the specifier. This means the outer function is noexcept exactly when the operation it wraps is noexcept.

cpp
// C++11

// swap is noexcept iff std::swap(a, b) is noexcept for the member types
template<typename T>
void swap(T& a, T& b) noexcept(noexcept(std::swap(a, b))) {
    std::swap(a, b);
}

// operator+ is noexcept iff operator+= is noexcept
template<typename T>
T operator+(T lhs, const T& rhs) noexcept(noexcept(lhs += rhs)) {
    lhs += rhs;
    return lhs;
}

// clone is noexcept iff T's copy constructor is noexcept
template<typename T>
T clone(const T& v) noexcept(std::is_nothrow_copy_constructible_v<T>) {
    return v;  // copy-initialises from v
}

When composing multiple operations, combine the conditions with && inside noexcept(...). The guarantee propagates: if every operation in a call chain is conditionally noexcept, the outermost function's noexcept(...) condition will evaluate to true only when all of them are actually non-throwing for the given T.

noexcept Destructors (Implicit and Explicit)

C++11 makes destructors implicitly noexcept unless a base or member destructor is explicitly noexcept(false). This is a deliberate language rule: throwing from a destructor during stack unwinding would call std::terminate, so the default guarantees safety. Opting out with noexcept(false) is valid syntax but nearly always wrong.

cpp
// C++11

struct NormalDtor {
    ~NormalDtor() { /* implicitly noexcept */ }
};

// Rare opt-out — the destructor is allowed to throw in non-unwinding contexts
struct SpecialDtor {
    ~SpecialDtor() noexcept(false) {
        might_throw();  // legal, but dangerous
    }
};

If a noexcept(false) destructor throws while another exception is active (i.e., during stack unwinding), std::terminate is called immediately. The correct pattern when a destructor must perform cleanup that might fail is to catch and suppress internally — logging to a side channel that cannot itself throw:

cpp
// C++11
struct SafeResource {
    ~SafeResource() noexcept {
        try {
            flush_and_close();  // might throw
        } catch (...) {
            // log to a non-throwing sink (e.g., write to fd directly)
            // never rethrow from a destructor
        }
    }
};

noexcept Speculation vs Guarantee

noexcept is a promise enforced at runtime, not a property verified by the compiler. Marking a function noexcept when it calls internally throwing functions compiles without any diagnostic — the compiler will not warn. If an exception does escape the noexcept boundary, std::terminate is called with no stack unwinding, making the failure harder to diagnose than an ordinary uncaught exception.

cpp
// C++11

// BAD: allocates memory internally — std::bad_alloc can escape
void load_config() noexcept {
    config_ = std::make_unique<Config>(read_file("config.json"));  // may throw!
}

// GOOD: explicitly documents that this function may throw
void load_config() noexcept(false) {  // same as omitting noexcept, but self-documenting
    config_ = std::make_unique<Config>(read_file("config.json"));
}

The asymmetry with [[nodiscard]] (C++17) is instructive: [[nodiscard]] is a use-site hint — the compiler can warn when the caller ignores a return value. noexcept has no analogous enforcement at the definition site. The closest debugging aid is running the program under AddressSanitizer or UBSan (-fsanitize=undefined) — neither catches a noexcept lie before runtime, but the std::terminate call from a violated noexcept is easy to identify in a crash report because std::terminate appears in the stack trace immediately above the throwing call.

noexcept and Compiler Optimizations

noexcept functions can be compiled without unwind tables, shrinking the binary and improving instruction-cache density on hot paths. When a function is noexcept, the compiler knows that no exception can escape, so it does not need to generate landing-pad code or register the function in the stack-unwind table. With -fno-exceptions the compiler treats every function this way globally; with exceptions enabled, only functions explicitly marked noexcept receive this treatment.

cpp
// C++11

// Without noexcept — compiler may emit a landing pad and unwind metadata
int add_risky(int a, int b) {
    return a + b;
}

// With noexcept — no landing pad; smaller object code, no EH table entry
int add_safe(int a, int b) noexcept {
    return a + b;
}

The practical effect is usually small for non-virtual calls where the optimizer can inline and see through the call boundary. The benefit is more measurable for virtual calls: a virtual dispatch through a noexcept function pointer (since C++17, noexcept is part of the type) lets the optimizer eliminate the exception path entirely in the caller. On toolchains that use DWARF-based zero-cost exception handling (Linux/ELF, the common case), the overhead is paid at throw time rather than at every function call, so the binary-size benefit of noexcept is the dominant effect rather than raw runtime throughput.

noexcept in Interfaces and Contracts

Apply noexcept surgically: always on destructors, swap, and move operations; never on functions that allocate or invoke user-supplied code. The decision rule is about whether the function has a narrow or wide contract — a narrow contract means the function can legally fail given its preconditions, making noexcept a liability rather than an asset.

cpp
// C++11 / C++20

// ALWAYS noexcept
~MyClass() noexcept;                                   // destructor
void swap(MyClass&) noexcept;                          // swap
MyClass(MyClass&&) noexcept;                           // move constructor
MyClass& operator=(MyClass&&) noexcept;                // move assignment
int id() const noexcept;                               // pure observer, no allocation

// CONSIDER noexcept — safe only if members are noexcept-copyable
[[nodiscard]] std::string name() const
    noexcept(std::is_nothrow_copy_constructible_v<std::string>);

// NEVER noexcept — allocates, may call user code
void push(const std::string& s);                       // may throw std::bad_alloc
template<typename Fn>
void apply(Fn&& fn) noexcept(noexcept(fn()));          // conditional — Fn decides

// C++20: [[nodiscard]] and noexcept work well together on observer functions
[[nodiscard]] bool empty() const noexcept;
[[nodiscard]] std::size_t size() const noexcept;

A practical decision table: destructors and swap — noexcept unconditionally; move operations — noexcept when only transferring ownership (pointer swaps, std::exchange); observer functions with no allocation — noexcept unconditionally; generic wrappers — conditional noexcept derived from traits; anything that calls new, malloc, or unknown user code — omit noexcept or use noexcept(false) as explicit documentation. The [[nodiscard]] attribute (C++17) and [[likely]]/[[unlikely]] (C++20) complement noexcept for complete function specification: [[nodiscard]] prevents silent discard of error codes, while noexcept removes the exception path entirely.

See Also