Performance[perf]
When to mark a move constructor noexcept in C++
- Section
- Performance [perf]
- Standard
- C++20
- Compilers
- MSVC 19.44, Clang 19.1
- Published
- Length
- 12 min read
In this answer9 clauses
- 1The short answer: moves and swap, when they cannot throw
- 2Why std::vector copies when your move might throw
- 3See it happen: count the copies when a vector grows
- 4What is already noexcept without your writing it
- 5Check it at compile time with static_assert
- 6Conditional noexcept for templates and wrappers
- 7When not to write noexcept
- 8noexcept in the type system since C++17
- 9A noexcept checklist for a new class
Abstract
Mark a move constructor, a move assignment and swap noexcept when they cannot throw, because std::vector and other library code move elements only when the move is noexcept or the type cannot be copied, and copy them otherwise. Implicit and defaulted moves are noexcept on their own whenever the members' moves are, so check with static_assert(std:: instead of guessing, and keep noexcept off anything that can throw, because an exception leaving it calls std::terminate.

Mark a move constructor, a move assignment operator and a swap function noexcept when they cannot throw, which is true of the usual move that takes over a pointer or a handle and resets the source. The mark changes what your program does: when a std::vector outgrows its storage, it moves the old elements only if their move constructor is noexcept or they cannot be copied, and copies them otherwise. If you write no special members at all, write no noexcept either: the compiler-written moves are noexcept whenever the members’ moves are. Check both cases with static_assert(std::, and keep noexcept off anything that can throw, because an exception that leaves a noexcept function calls std::terminate.
This checklist is written for C++20. noexcept arrived in C++11 and replaced dynamic exception specifications, which C++11 deprecated and C++17 removed; C++17 kept throw() as a deprecated spelling of noexcept(true), and C++20 removed that too. C++17 also made the exception specification part of a function’s type. The list of functions that are noexcept without a mark is C++11’s, plus the defaulted comparison operators of C++20. Every sample was compiled with MSVC 19.44 (/std:c++20 /EHsc /W4 /permissive-) and Clang 19.1 (-std=c++20 -Wall -Wextra, on Microsoft’s standard library) on one Windows x64 machine, without optimization unless the text says otherwise; the outputs, counts and exit codes shown are what those builds produced there, and every count is what both printed. The C++11 table of Since When gives noexcept from GCC 4.6, Clang 3.0 and Visual Studio 2015.
What changed: a function that cannot throw is marked throw()noexcept.
since C++11
The short answer: moves and swap, when they cannot throw
A class that has to write its own special members, with the marks where they belong:
class Matrix {
public:
Matrix(std::size_t rows, std::size_t cols)
: rows_(rows), cols_(cols), data_(new double[rows * cols]{}) {}
~Matrix() { delete[] data_; } // noexcept without being told
Matrix(const Matrix& other) // allocates, so it may throw
: Matrix(other.rows_, other.cols_) {
std::copy_n(other.data_, rows_ * cols_, data_);
}
Matrix& operator=(const Matrix& other) { // may throw too
Matrix tmp(other);
swap(tmp);
return *this;
}
Matrix(Matrix&& other) noexcept // takes the buffer: can't throw
: rows_(std::exchange(other.rows_, 0)),
cols_(std::exchange(other.cols_, 0)),
data_(std::exchange(other.data_, nullptr)) {}
Matrix& operator=(Matrix&& other) noexcept {
Matrix tmp(std::move(other));
swap(tmp);
return *this;
}
void swap(Matrix& other) noexcept {
std::swap(rows_, other.rows_);
std::swap(cols_, other.cols_);
std::swap(data_, other.data_);
}
friend void swap(Matrix& a, Matrix& b) noexcept { a.swap(b); }
private:
std::size_t rows_, cols_;
double* data_;
};
static_assert(std::is_nothrow_move_constructible_v<Matrix>);
static_assert(std::is_nothrow_move_assignable_v<Matrix>);
static_assert(std::is_nothrow_swappable_v<Matrix>);
static_assert(!std::is_nothrow_copy_constructible_v<Matrix>);— end example]
Each mark follows from what the function does:
- The destructor needs none: destructors are
noexceptby default. - The copy constructor and copy assignment allocate, so they may throw, and they stay unmarked. The copy assignment builds the copy before it touches
*this, so a throw leaves the target as it was. - The move constructor and move assignment exchange a pointer and two sizes, so they are
noexcept. The Core Guidelines ask for exactly this in C.66, “Make move operations noexcept”. swapexchanges three scalars, so it isnoexcepttoo (C.85).
A real matrix would hold a std::vector<double> and declare none of this. Under the rule of zero, its compiler-written moves would be noexcept with no marks at all.
Why std::vector copies when your move might throw
push_back and emplace_back, and an insert or emplace of one element at the end, make a promise in the standard, in [vector.modifiers]: if an exception is thrown while inserting a single element at the end, and T is copy-insertable or std:: is true, there are no effects. When the vector is full, the insertion has to build every old element again in new storage before the old storage goes.
Moving the old elements could break that promise. If the move of the fifth element throws, the first four originals are already moved-from, and moving them back could throw too. Copying leaves the originals untouched: if a copy throws, the vector destroys the copies, frees the new block and is as it was. So:
- when the move may throw and the type can be copied, the vector copies;
- when the move is
noexcept, moving can’t fail, and it moves; - when the type can’t be copied at all, it uses the throwing move, and if that throws, the standard says the effects are unspecified.
The library tool for this choice is std:: (C++11, in <utility>). It returns T&& when std::, and const T& otherwise, and cppreference names std::vector::resize as one of its users. The standard specifies the guarantee, not the tool, so the next section counts what a real library does. What a move is, and what std::move itself does, is covered in move semantics, std::move and rvalue references.
See it happen: count the copies when a vector grows
#include <cstdio>
#include <vector>
template <bool NoexceptMove>
struct Counted {
static inline int copies = 0;
static inline int moves = 0;
Counted() = default;
Counted(const Counted&) { ++copies; }
Counted(Counted&&) noexcept(NoexceptMove) { ++moves; }
};
struct MoveOnly { // no copy, and a move that may throw
static inline int copies = 0;
static inline int moves = 0;
MoveOnly() = default;
MoveOnly(const MoveOnly&) = delete;
MoveOnly(MoveOnly&&) { ++moves; }
};
template <class T>
void grow(const char* label, bool reserve_first) {
T::copies = T::moves = 0;
std::vector<T> v;
if (reserve_first) v.reserve(1000);
for (int i = 0; i < 1000; ++i) v.push_back(T{});
std::printf("%-24s %4d copies, %4d moves\n", label, T::copies,
T::moves);
}
int main() {
grow<Counted<false>>("move may throw:", false);
grow<Counted<true>>("move noexcept:", false);
grow<Counted<false>>("may throw + reserve:", true);
grow<MoveOnly>("move-only, may throw:", false);
}— end example]
What it printed:
| Element type | Copies | Moves |
|---|---|---|
| Move may throw | 2,137 | 1,000 |
Move is noexcept |
0 | 3,137 |
Move may throw, after reserve |
0 | 1,000 |
| Move-only, move may throw | 0 | 3,137 |
In every row, 1,000 moves are the arguments: push_back(T&&) moves its argument into place whatever the move’s noexcept says. The rest is relocation, 2,137 elements built again as the vector grew: copied when the move may throw, moved when it can’t. After reserve(1000) there is nothing to relocate. The move-only type is moved anyway, since there is no copy to fall back on.
These numbers came from MSVC 19.44 and Clang 19.1, both on Microsoft’s STL, built without optimization and again with /O2 and -O2; all four builds printed the same table. The 2,137 depends on how much the vector grows each time, which the standard leaves to the implementation, so another library prints other totals; copy versus move follows from the guarantee. The program counts, it doesn’t time. Whether the copies cost your program anything depends on the type and the sizes, and a benchmark library from Toolchain answers that for your own code.
What is already noexcept without your writing it
The noexcept specifier page on cppreference, following [except.spec] in the draft, lists the functions that are non-throwing without a mark:
- destructors, hand-written or not, unless a base or member destructor is potentially throwing (what happens when a destructor throws covers that case);
- default, copy and move constructors and copy and move assignment operators that are implicitly declared or defaulted on their first declaration, unless something they would call is potentially throwing;
- comparison operators defaulted on their first declaration (C++20), on the same condition;
- deallocation functions such as
operator delete.
Everything else is potentially throwing until you mark it, and the compiler doesn’t read the body to find out:
struct Plain { // rule of zero: nothing declared
std::string name;
std::vector<int> values;
};
static_assert(std::is_nothrow_move_constructible_v<Plain>);
static_assert(std::is_nothrow_move_assignable_v<Plain>);
static_assert(std::is_nothrow_destructible_v<Plain>);
static_assert(!std::is_nothrow_copy_constructible_v<Plain>);
struct Defaulted {
Defaulted(Defaulted&&) = default; // noexcept, worked out
std::string name; // from the members
};
static_assert(std::is_nothrow_move_constructible_v<Defaulted>);
struct DefaultedLater {
DefaultedLater(DefaultedLater&&); // no noexcept here...
std::string name;
};
DefaultedLater::DefaultedLater(DefaultedLater&&) = default;
static_assert(!std::is_nothrow_move_constructible_v<DefaultedLater>);
struct HandWritten {
HandWritten(HandWritten&& other) : p(other.p) { other.p = nullptr; }
int* p = nullptr; // the body can't throw, but nobody said so
};
static_assert(!std::is_nothrow_move_constructible_v<HandWritten>);
struct Cleans {
~Cleans() { std::puts("cleanup"); } // hand-written, still noexcept
};
static_assert(std::is_nothrow_destructible_v<Cleans>);
static_assert(noexcept(std::declval<Cleans&>().~Cleans()));— end example]
DefaultedLater is the surprise: its move constructor is defaulted, but not on its first declaration, so it counts as user-provided and takes the exception specification of its declaration, which is none.
Because the members decide, their types matter. The standard declares std::vector’s move constructor noexcept (since C++17) and std::basic_string’s (since C++11), but not std::list’s ([list.overview]). Microsoft’s STL, the library compiled against here, declares its std::list move constructor noexcept(false) in <list>, and its body allocates a new sentinel node that the moved-from list keeps; its std::deque move constructor has no noexcept either. On that library a class with a list member has a potentially throwing move, and a vector of it copies when it grows:
struct Tasks {
std::list<int> pending; // Microsoft's STL: move is noexcept(false)
};
static_assert(!std::is_nothrow_move_constructible_v<Tasks>);— end example]
On another library, check rather than assume.
Check it at compile time with static_assert
Put the checks right after the class, so a member change that makes a move potentially throwing breaks the build in the header, not a vector’s moves somewhere else:
static_assert(std::is_nothrow_move_constructible_v<Matrix>);
static_assert(std::is_nothrow_move_assignable_v<Matrix>);
static_assert(std::is_nothrow_swappable_v<Matrix>); // C++17
static_assert(std::is_nothrow_destructible_v<Matrix>);
static_assert(noexcept(std::declval<Matrix&>().swap(
std::declval<Matrix&>())));— end example]
The traits ask whether T(std::move(t)) or t = std::move(u) can throw, whichever function that picks. So they also catch the class whose moves quietly disappeared, for instance after someone added a destructor, and fell back to a copy that allocates. The noexcept operator asks the same of any expression without evaluating it. Both read declarations only: an assertion proves the promise is made, not that the body keeps it.
Conditional noexcept for templates and wrappers
In a template, whether a move can throw depends on T. Let the compiler work it out where it can: a defaulted member’s exception specification is computed from the members, so = default is the whole answer for the moves. swap can’t be defaulted, so it gets the condition written out:
template <class T>
class Box {
public:
explicit Box(T value) : value_(std::move(value)) {}
Box(Box&&) = default; // noexcept exactly when T's is
Box& operator=(Box&&) = default; // likewise
friend void swap(Box& a, Box& b)
noexcept(std::is_nothrow_swappable_v<T>) {
using std::swap; // std::swap, or T's own swap found by ADL
swap(a.value_, b.value_);
}
private:
T value_;
};
static_assert(std::is_nothrow_move_constructible_v<Box<std::string>>);
static_assert(!std::is_nothrow_move_constructible_v<Box<Risky>>);
static_assert(std::is_nothrow_swappable_v<Box<std::string>>);
static_assert(!std::is_nothrow_swappable_v<Box<Risky>>);— end example]
Risky there is any type with a move that isn’t noexcept. When the body has to be your own, put the condition in the specifier:
template <class T>
class Logged {
public:
explicit Logged(T value) : value_(std::move(value)) {}
Logged(Logged&& other)
noexcept(std::is_nothrow_move_constructible_v<T>)
: value_(std::move(other.value_)) {
++moves; // the extra work that rules out = default
}
static inline int moves = 0;
private:
T value_;
};— end example]
In a function template the condition is often whether an expression can throw, and that is spelled with noexcept twice: the outer one is the specifier, the inner one is the operator, which asks without evaluating anything.
template <class F>
void call_twice(F& f) noexcept(noexcept(f())) {
f();
f();
}— end example]
std:: (C++17) says the same as noexcept(f()) here. And std::swap itself is noexcept exactly when T’s move constructor and move assignment are, so a type that gets its moves right gets a noexcept std::swap with them.
When not to write noexcept
When the function can throw. noexcept doesn’t stop an exception; it turns one that would leave the function into a call to std::terminate ([except.terminate]), and whether the stack is unwound first is implementation-defined:
#include <cstdio>
#include <stdexcept>
void parse(int x) {
if (x < 0) throw std::invalid_argument("negative");
}
void load(int x) noexcept { // a promise parse() can break
parse(x);
}
int main() {
try {
load(-1);
} catch (const std::exception& e) {
std::printf("caught: %s\n", e.what()); // never runs
}
std::puts("done"); // never runs either
}— end example]
Neither compiler warned at /W4 or -Wall -Wextra, since the throw is in another function. Both programs printed nothing and ended with status 0xC0000409: the catch never ran. By default std::terminate calls std::abort.
When a move has to allocate or call something that may throw. A type whose moved-from state needs memory of its own, like Microsoft’s std::list, has a move that can fail. Leave the mark off and let std::vector copy it, which is correct.
When you might want the promise back. noexcept is part of the interface: callers can static_assert it, build their own noexcept conditions on it, store the function in a pointer whose type says noexcept (since C++17), and std::vector chooses moves because of it. Taking it out later breaks the assertions and the pointers at compile time and quietly changes the rest: conditions built on it flip to false, and the vector goes back to copying. Promise it where the function cannot throw by design, as moves, swaps and destructors usually can’t, not because today’s body happens not to.
Not as a speed switch. What noexcept visibly changes is the choices library code makes, as counted above; what it does to the generated code is up to the compiler, so measure before you mark a function for speed.
noexcept in the type system since C++17
void reset() noexcept;
void flush();
static_assert(!std::is_same<decltype(reset), decltype(flush)>::value,
"different types since C++17");
void (*any_fn)() = reset; // fine: the promise is dropped
// void (*safe_fn)() noexcept = flush; // error since C++17— end example]
Built as C++14, both compilers treat reset and flush as the same type; from C++17 on they differ, and a function pointer converts only from noexcept to plain (uncommenting the last line gives MSVC error C2440 and Clang’s “different exception specifications”). The rule that a noexcept virtual function’s overriders must be noexcept too is older, from C++11.
What breaks in old code is a template that takes a function type apart: a partial specialization for R(A...) no longer matches a noexcept function. Add one for R(A...) noexcept, behind the feature-test macro __cpp_noexcept_function_type if the code still builds as C++14:
template <class F> struct arity; // number of parameters of F
template <class R, class... A>
struct arity<R(A...)> {
static constexpr std::size_t value = sizeof...(A);
};
#if __cpp_noexcept_function_type // C++17: noexcept is in the type
template <class R, class... A>
struct arity<R(A...) noexcept> : arity<R(A...)> {};
#endif
static_assert(arity<decltype(flush)>::value == 0);
static_assert(arity<decltype(reset)>::value == 0); // needs the #if part— end example]
Without the second specialization, both compilers reject arity<decltype(reset)> in C++17 as an undefined template. In C++14 the second specialization would redefine the first, which both compilers report, so the #if keeps it out there.
A noexcept checklist for a new class
- Follow the rule of zero where you can: declare no special members, and the compiler’s moves are
noexceptwhenever the members’ are. - Mark a hand-written move constructor and move assignment
noexceptwhen they only transfer ownership. - If the class has its own
swap, member or friend, mark itnoexcept(C.85). - Leave the destructor unmarked: it is
noexceptalready. - Leave copies, and anything that allocates, does I/O or calls code that may throw, unmarked.
- In templates,
= defaultthe moves, or writenoexcept(std::where the body has to be yours.is_nothrow_move_constructible_v<T>) static_asserttheis_nothrowtraits right after the class.- Run clang-tidy’s performance-noexcept-move-constructor and
performance-noexcept-swap. On a class that left the marks off, clang-tidy 19.1.5 reported “move constructors should be marked noexcept”, “move assignment operators should be marked noexcept” and “swap functions should be marked noexcept”.
More on copies, moves and allocation is in Performance.
Questions
Should move constructors always be noexcept?
When they cannot throw, yes, and a move that only takes over pointers or handles cannot. If a move has to allocate or call something that may throw, leave noexcept off: std::vector then copies instead, which is correct, while noexcept would turn the exception into std::terminate. The C++ Core Guidelines say the same in C.66.
Is a defaulted move constructor noexcept?
Yes, when every move it calls is: a move constructor that is implicitly declared, or defaulted on its first declaration, is noexcept exactly when the bases' and members' moves are. One declared in the class and defaulted outside it is user-provided and takes the exception specification of its declaration, so without noexcept there it is potentially throwing. static_assert(std:: tells you which you have.
What happens if a noexcept function throws?
When the exception would leave the function, std::terminate is called, and whether the stack is unwound first is implementation-defined. No catch outside the function sees the exception, and by default std::terminate calls std::abort. Compilers do not check the promise across calls: neither compiler here warned about a noexcept function that calls one that throws.
Why does std::vector copy instead of move when it grows?
Because push_back and emplace_back promise no effects if an exception is thrown, and that holds only if moving the old elements into new storage cannot fail halfway. When the element’s move constructor may throw and the element can be copied, the vector copies and leaves the originals intact; when the move is noexcept, or the type cannot be copied, it moves. Mark the move noexcept if it cannot throw, or call reserve() when you know the size.
Should swap be noexcept?
Yes, when it exchanges pointers, handles or members whose own swaps are noexcept, which is the usual case; the Core Guidelines ask for it in C.85. std::swap is noexcept when the type’s move constructor and move assignment are, and std:: (C++17) checks the swap that a using std::swap; call would find.
Sources
- noexcept specifier (since C++11)cppreference.com
- std::move_if_noexceptcppreference.com
- [vector.modifiers] ModifiersC++ standard working draft
- C++ Core Guidelines C.66: Make move operations noexceptC++ Core Guidelines