iostream formatting
noexcept is a promise in the function's type: this call will not throw. The compiler can rely on it, the standard library does rely on it for moves and swaps, and if the promise is broken the program terminates instead of unwinding.
The keyword is part of why the containers are fast: a vector moves its elements during a reallocation only if the move constructor is noexcept, because a throwing move would leave the vector unable to restore the old elements. This lesson measures that choice and ends with the terminate it protects against.
The keyword is part of the function's type, so a pointer to a noexcept function and a pointer to a throwing one are different types. That is what lets the library choose an algorithm from the declaration alone, without looking at the body.
A noexcept function
The keyword goes after the parameter list. It does not change what the function does, only what the compiler may assume, and noexcept(expression) asks the same question at compile time.
The program prints whether a simple arithmetic function and a function without the keyword are noexcept. The answers are 1 and 0, and the second one is only a statement about the declaration, not about the body.
A function that allocates, calls a virtual function or uses a library that throws is not noexcept, and marking it so is a bug that appears at run time. The compile-only section shows what the compiler says about it.
#include <iostream>
int add(int a, int b) noexcept
{
return a + b;
}
int multiply(int a, int b)
{
return a * b;
}
int main()
{
std::cout << noexcept(add(1, 2)) << ' ' << noexcept(multiply(1, 2)) << '\n';
}
g++ -std=c++23 -Wall -Wextra promise.cpp -o promise
./promise
1 0
Breaking the promise
A throw inside a noexcept function cannot be handled by the caller. The compiler sees it, warns that the throw will always call terminate, and compiles the function anyway.
The program below is compiled but not run: running it prints the terminate message and aborts the process, which is the defined behaviour and not a crash the program can catch.
The warning is the useful part. It appears with -Wall, names the throw and the function, and it is the reason a noexcept added for speed should be checked against the body.
#include <stdexcept>
void risky() noexcept
{
throw std::runtime_error{"no"};
}
int main()
{
risky();
}
g++ -std=c++23 -Wall -Wextra breaking.cpp -o breaking
breaking.cpp: In function 'void risky()':
breaking.cpp:5:5: warning: 'throw' will always call 'terminate' [-Wterminate]
5 | throw std::runtime_error{"no"};
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Why the containers need it
A vector that grows copies its elements when their move constructor is not noexcept. The standard says the vector may only move if that cannot throw, because a half-moved buffer could not be restored if the move failed.
The program's item has a copy constructor and a move constructor that is not noexcept. Two push_back calls on a vector with one slot reserved force a reallocation, and the counters show copies instead of moves.
Marking the move constructor noexcept turns the same program into three moves and no copy. That single keyword is the difference between a reallocation that copies a buffer and one that swaps pointers.
#include <iostream>
#include <vector>
int copies = 0;
int moves = 0;
class item
{
public:
item() = default;
item(const item&)
{
++copies;
}
item(item&&)
{
++moves;
}
};
int main()
{
std::vector<item> items;
items.reserve(1);
items.push_back(item{});
items.push_back(item{});
std::cout << "copies " << copies << " moves " << moves << '\n';
}
g++ -std=c++23 -Wall -Wextra vector.cpp -o vector_noexcept
./vector_noexcept
copies 1 moves 2
A conditional noexcept
A template can pass the promise on: noexcept(noexcept(expression)) says that the function is noexcept exactly when the expression inside is. That is how a wrapper keeps the property of what it wraps.
The program's call is noexcept for a function that cannot throw and not noexcept for one that can, and it prints both answers for the same template.
The standard library uses the form throughout: std::vector's move constructor is noexcept when the allocator's move is, and std::swap is noexcept when the moves are.
#include <iostream>
void safe() noexcept
{
}
void unsafe()
{
}
template <typename F>
void call(F f) noexcept(noexcept(f()))
{
f();
}
int main()
{
std::cout << noexcept(call(safe)) << ' ' << noexcept(call(unsafe)) << '\n';
}
g++ -std=c++23 -Wall -Wextra condition.cpp -o condition
./condition
1 0
The promise a swap needs
Copy and swap from lesson 078 relies on a swap that cannot throw. The trait std::is_nothrow_swappable answers whether a type provides one, and a static_assert turns the answer into a build error when it changes.
The program's buffer has a noexcept move constructor and assignment, so both traits are true and the printed values are 1.
A type with a user-defined move that allocates would answer false, and the containers would copy it instead of moving. The trait is the check, and the keyword is the fix.
#include <iostream>
#include <type_traits>
#include <utility>
class buffer
{
public:
explicit buffer(int size) : size_(size), data_(new int[size]{}) {}
buffer(const buffer&) = default;
buffer& operator=(const buffer&) = default;
buffer(buffer&& other) noexcept : size_(other.size_), data_(other.data_)
{
other.size_ = 0;
other.data_ = nullptr;
}
buffer& operator=(buffer&& other) noexcept
{
std::swap(size_, other.size_);
std::swap(data_, other.data_);
return *this;
}
~buffer()
{
delete[] data_;
}
private:
int size_;
int* data_;
};
int main()
{
static_assert(std::is_nothrow_move_constructible_v<buffer>);
static_assert(std::is_nothrow_swappable_v<buffer>);
std::cout << std::is_nothrow_move_constructible_v<buffer> << ' '
<< std::is_nothrow_swappable_v<buffer> << '\n';
}
g++ -std=c++23 -Wall -Wextra swap.cpp -o swap_noexcept
./swap_noexcept
1 1
Exercises
- Mark the move constructor in vector.cpp noexcept and compare the counters.
- Write a function with a conditional noexcept that wraps std::stoi and print the trait.
- Remove noexcept from buffer's move constructor and see which static_assert fails.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
noexcepton a function mean?Answer
The function promises not to let an exception escape
The promise is part of the type, and breaking it calls std::terminate.
-
What does
noexcept(expression)give?Answer
A bool known at compile time
The operator asks whether the expression is declared not to throw.
-
Why does a vector move its elements during reallocation?
Answer
The move is noexcept, so a failure can be undone by keeping the old block
Without a no-fail move, the vector copies to keep the strong guarantee.
-
What happens when a noexcept function throws?
Answer
std::terminate is called
The runtime cannot unwind through the promise, so it terminates.
-
What does this program print?
#include <iostream> void good() noexcept {} void bad() {} int main() { std::cout << noexcept(good()) << ' ' << noexcept(bad()); }Answer
1 0
The noexcept operator reports whether each call is declared not to throw.
-
What does the compiler warn about in this function?
#include <stdexcept> void risky() noexcept { throw std::runtime_error{"no"}; } int main() { risky(); }Answer
'throw' will always call 'terminate'
A throw inside a noexcept function cannot propagate, and the compiler warns about the terminate.
-
What does this program print?
#include <iostream> #include <utility> #include <vector> int copies = 0, moves = 0; struct item { item() = default; item(const item&) { ++copies; } item(item&&) noexcept { ++moves; } }; int main() { std::vector<item> values; values.push_back(item{}); values.push_back(item{}); values.push_back(item{}); std::cout << "copies " << copies << " moves " << moves; }Answer
copies 0 moves 6
Each push moves its temporary, and each reallocation moves the elements already stored, so the copies stay zero.
-
What does this program print?
#include <iostream> #include <type_traits> void may_throw(); void no_throw() noexcept; int main() { std::cout << noexcept(no_throw()) << ' ' << std::is_nothrow_move_constructible_v<std::string>; }Answer
1 1
The declared function is noexcept, and the string's move constructor is nothrow on this build.
-
What does this program print?
#include <iostream> #include <utility> struct value { int number; void swap(value& other) noexcept { std::swap(number, other.number); } }; int main() { value a{1}, b{2}; a.swap(b); std::cout << noexcept(a.swap(b)) << ' ' << (a.number == 2); }Answer
1 1
The swap is declared noexcept, and it exchanges the two numbers.
-
Why should a move constructor be marked noexcept?
Answer
So containers move instead of copy during growth
The container checks the trait and falls back to copying when the move may throw.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev