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.

See Also