noexcept: Copy vs Move
A single keyword — noexcept — can change STL container behavior from O(n) copies to O(1) pointer transfers. Here is why the C++ standard library cares deeply about exception specifications.
The Core Problem
When a std::vector runs out of capacity and reallocates, it must transfer all existing elements into the new larger buffer. The question is: should it copy or move them?
Copying is safe — if something goes wrong halfway through, the original buffer still has all its data intact. But moving is faster — it just transfers ownership of internal resources (pointers, handles) without duplicating data.
The problem: if a move throws an exception halfway through reallocation, both the old buffer (partially moved-out) and the new buffer are corrupted. You have lost data. So std::vector will only use move if the move constructor is marked noexcept.
Without noexcept — Forced to Copy
Without the noexcept specifier, the container cannot trust that the move will succeed. It conservatively copies every element — O(n) allocations each time the vector grows.
// No noexcept — vector is FORCED to copy on reallocation
class OrderBook {
public:
std::vector<Order> orders;
// Move constructor WITHOUT noexcept
OrderBook(OrderBook&& other) {
orders = std::move(other.orders);
}
// std::vector cannot use this move — it must fall back to copy
// because if an exception is thrown mid-reallocation, it needs
// to guarantee the strong exception safety invariant.
};With noexcept — Safe to Move
Adding noexcept is a compile-time contract: “this function will never throw.” The STL checks this at compile time using type traits and enables the O(1) move path.
// With noexcept — vector can SAFELY move on reallocation
class OrderBook {
public:
std::vector<Order> orders;
// Move constructor WITH noexcept — signals: "I will not throw"
OrderBook(OrderBook&& other) noexcept {
orders = std::move(other.orders); // O(1) pointer transfer
}
// std::vector sees noexcept guarantee, picks move over copy.
// Result: O(1) reallocation instead of O(n).
};How the STL Decides at Compile Time
// How std::vector decides at compile time using type traits:
if constexpr (
std::is_nothrow_move_constructible<T>::value // noexcept move?
|| !std::is_copy_constructible<T>::value // or no copy?
) {
// MOVE — O(1): transfer pointer + size + capacity
new_buffer[i] = std::move(old_buffer[i]);
} else {
// COPY — O(n): duplicate every element into new buffer
new_buffer[i] = old_buffer[i];
}“The type trait std::is_nothrow_move_constructible<T> evaluates to true only if T's move constructor is declared noexcept. This check happens entirely at compile time — zero runtime overhead.”
noexcept: Copy vs Move
C++ containers move objects only when it is guaranteed safe—determined at compile time by the noexcept specifier on the move constructor.
◂ old_buf [cap=4]
▸ new_buf [cap=8]
orderbook.hpp
struct OrderBook {
std::vector<Order> orders;
// no move ctor declared
// vector uses copy ctor
// on every reallocation
};
ctor
copy ctor
allocs
5×
cost
O(n)
◂ old_buf [cap=4]
▸ new_buf [cap=8]
orderbook.hpp
struct OrderBook {
std::vector<Order> orders;
OrderBook(OrderBook&& o)
noexcept
{
orders = std::move(o.orders);
}
};
ctor
move ctor
allocs
1×
cost
O(1)
One keyword can change how the entire container behaves.
When to Mark noexcept
No comments yet. Be the first to contribute.