Question
When to Use noexcept in C++: Practical Rules, Performance, and Best Practices
Question
In C++, the noexcept keyword can be added to many function declarations, but it is not always clear when it should be used in practice.
I understand that noexcept became especially important for cases such as move constructors, where throwing can affect how standard library containers behave. However, I still have several practical questions:
- If I have functions that I know will never throw, but the compiler cannot prove that fact, should I mark all of them with
noexcept? - It seems impractical to analyze every single function declaration in a codebase. In which situations should I be especially careful to use
noexcept, and when is leaving the default behavior acceptable? - When does
noexceptrealistically improve performance? - Can you give an example where adding
noexceptlets a C++ compiler generate better code? - Do modern C++ compilers and the standard library actually take advantage of
noexceptfor optimization, especially around move operations?
For example, consider a simple function like this:
int square(int x) noexcept {
return x * x;
}
and compare it with a similar function without noexcept:
int square(int x) {
return x * x;
}
When does the noexcept promise matter in real code, and how should I decide where to use it?
Short Answer
By the end of this page, you will understand what noexcept means in C++, when it is worth adding, why it matters for move constructors and standard containers, and where it usually does not produce meaningful speedups. You will also learn practical rules for using it safely in real codebases.
Concept
noexcept is a promise: this function will not allow an exception to escape.
A function marked noexcept can still contain code that might throw internally, but if an exception does escape the function, the program calls std::terminate() instead of normal exception propagation.
void f() noexcept {
throw 42; // Compiles, but if this exception escapes, the program terminates.
}
That means noexcept is not just documentation. It changes program behavior.
Why noexcept matters
In modern C++, noexcept is most important in places where the standard library or generic code needs to decide between:
- moving an object
- copying an object
- or refusing an operation entirely
A classic example is std::vector. When a vector grows and must relocate its elements, it prefers moving them for efficiency. But if moving might throw and copying is available, the vector may choose copying instead, because copying can preserve stronger exception safety guarantees.
If a type has a move constructor marked noexcept, containers can safely use it during reallocation.
What noexcept does not mean
It does not mean:
- the function is always faster
- the compiler can magically optimize everything better
- every non-throwing function should always be marked immediately
In practice, noexcept is most valuable where exception safety affects overload resolution, generic behavior, container performance, and API contracts.
The key idea
Use noexcept when non-throwing behavior is an important and stable part of the function's contract.
This is especially true for:
- move constructors
- move assignment operators
- swap functions
- destructors
- small low-level utility functions that must not fail
- functions used by generic code that checks
noexcept
For ordinary business logic functions, leaving them without noexcept is often fine unless you specifically want to make and enforce that guarantee.
Mental Model
Think of noexcept as putting a sealed "no exceptions may leave this room" sign on a function.
- Without the sign, an exception can leave the room and be handled somewhere else.
- With the sign, if an exception tries to leave, the building alarm goes off and the program is immediately stopped with
std::terminate().
Why would you ever want that sign?
Because other parts of the program can trust it.
For example, a std::vector moving elements during resize wants to know:
- "If I move these objects, can something fail halfway through?"
- If the answer is no, it can move confidently.
- If the answer is maybe, it may choose a safer but slower strategy, such as copying.
So noexcept is not mainly about saving a few CPU instructions. It is about giving strong guarantees that other code can rely on.
Syntax and Examples
Basic syntax
void logMessage() noexcept {
// guaranteed not to let exceptions escape
}
You can also make it conditional:
template <typename T>
void swapValues(T& a, T& b) noexcept(noexcept(T(std::move(a)))) {
T temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}
The inner noexcept(...) checks whether an expression can throw. The outer noexcept(...) uses that result to declare whether the function itself is non-throwing.
Beginner-friendly example
#include <iostream>
class Counter {
public:
Counter(int v) noexcept : value(v) {}
int get() const noexcept {
return value;
}
void increment() noexcept {
++value;
}
private:
int value;
};
int main() {
Counter c(5);
c.increment();
std::cout << c.get() << '\n';
}
This is a good use of noexcept because:
- constructing from an
intcannot throw here - reading the value cannot throw
- incrementing an
intcannot throw
These are simple operations with a clear non-throwing contract.
Important example: move constructor
#include <vector>
#include <string>
class Buffer {
public:
Buffer() = default;
Buffer(Buffer&& other) noexcept
: data(std::move(other.data)) {}
Buffer& operator=(Buffer&& other) noexcept {
data = std::move(other.data);
return *this;
}
private:
std::string data;
};
Marking move operations noexcept is often important because containers like std::vector<Buffer> can then move Buffer objects during reallocation instead of falling back to copying.
Conditional noexcept
This is common in templates:
#include <utility>
template <typename T>
class Box {
public:
Box(Box&& other) noexcept(noexcept(T(std::move(other.value))))
: value(std::move(other.value)) {}
private:
T value;
};
This means: the move constructor of Box<T> is noexcept only if moving T is also noexcept.
That is how generic libraries preserve correctness while still giving strong guarantees when possible.
Step by Step Execution
Consider this example:
#include <iostream>
#include <vector>
class Item {
public:
Item() = default;
Item(const Item&) {
std::cout << "copy\n";
}
Item(Item&&) noexcept {
std::cout << "move\n";
}
};
int main() {
std::vector<Item> items;
items.reserve(1);
items.push_back(Item{});
items.push_back(Item{});
}
What happens step by step
1. items.reserve(1);
The vector allocates enough space for 1 Item.
2. items.push_back(Item{});
A temporary Item is created.
The vector places it into its storage. Since the temporary is an rvalue, moving is used.
Output:
move
3. items.push_back(Item{});
Now the vector is full. It needs more storage.
To grow, it allocates a new larger block and relocates the existing element.
Because Item's move constructor is marked noexcept, the vector can safely move the existing element into the new storage.
Then it moves the new temporary into place.
Typical output:
move
move
move
One move comes from the first insertion. Two more moves happen during the second insertion:
- move old element to new storage
- move new temporary into new storage
What if the move constructor were not noexcept?
If the move constructor might throw and a copy constructor is available, many standard library implementations will prefer copying existing elements during reallocation.
That changes behavior conceptually to:
move
copy
move
The exact output depends on implementation details, but the important lesson is this:
noexcepton move operations affects how containers manage elements- this can influence both exception safety and performance
Real World Use Cases
1. Move-enabled classes stored in containers
If you write a class that owns resources and instances will live inside std::vector, std::deque, or similar containers, noexcept on move operations is often important.
Examples:
- file buffer wrappers
- image or audio buffers
- network packet objects
- parsed data records
2. swap functions
A custom swap is often expected to be non-throwing.
class Widget {
public:
void swap(Widget& other) noexcept {
std::swap(id, other.id);
}
private:
int id{};
};
Algorithms and generic code often work better when swap is known not to throw.
3. Destructors and cleanup logic
Destructors should not throw. In modern C++, destructors are effectively non-throwing by default unless declared otherwise, and throwing from a destructor during stack unwinding is dangerous.
Typical examples:
- releasing memory
- closing file handles
- unlocking mutexes
4. Low-level utility functions
Small functions that simply inspect state or perform arithmetic are often good candidates.
class Point {
public:
int x() const noexcept { return x_; }
int y() const noexcept { return y_; }
private:
int x_{};
int y_{};
};
5. Generic library code
Template code often uses noexcept to preserve guarantees from the types it works with.
Examples:
- containers
- smart pointer utilities
- tuple/pair helpers
- custom wrapper types
Real Codebase Usage
In real projects, developers usually do not try to mark every function noexcept.
Instead, they focus on places where it matters most.
Common patterns
1. Mark move operations noexcept when true
class Session {
public:
Session(Session&&) noexcept = default;
Session& operator=(Session&&) noexcept = default;
};
This is one of the most common and valuable uses.
2. Mark swap as noexcept
class Session {
public:
void swap(Session& other) noexcept {
std::swap(handle, other.handle);
}
private:
int handle{};
};
3. Use conditional noexcept in templates
template <typename T>
void resetToDefault(T& value) noexcept(noexcept(T{})) {
value = T{};
}
This lets generic code stay accurate.
4. Use it for small accessors and utility functions
class Config {
public:
bool enabled() const noexcept { return enabled_; }
private:
bool enabled_{};
};
5. Avoid it in code that may reasonably evolve to throw
For example, parsing, validation, file loading, database calls, and network code often should not be marked noexcept, because failure is part of the domain.
std::string loadConfigFile(const std::string& path); // not noexcept
Guard clauses and error handling
If a function is marked noexcept, you must handle all failures internally.
That often means:
- returning error codes
- storing error state
- logging and terminating
- checking preconditions up front
bool tryParseInt(const std::string& s, int& out) noexcept {
try {
out = std::stoi(s);
return true;
} catch (...) {
return false;
}
}
This is a deliberate API choice: errors do not escape as exceptions.
Practical rule used in codebases
A useful team rule is:
- Use
noexceptfor operations that conceptually cannot fail - Prioritize move operations, swap, destructors, and trivial accessors
- Do not add it speculatively to complex functions
Common Mistakes
1. Marking a function noexcept just because it usually does not throw
A function should be noexcept only if you are willing to make a strong promise that exceptions will never escape.
Broken example:
std::string readFile(const std::string& path) noexcept {
std::ifstream file(path);
if (!file) {
throw std::runtime_error("Cannot open file");
}
return "data";
}
If an exception escapes, the program terminates.
Better:
std::string readFile(const std::string& path) {
std::ifstream file(path);
if (!file) {
throw std::runtime_error("Cannot open file");
}
return "data";
}
2. Assuming noexcept automatically makes code faster
Usually, the biggest practical performance effect is through library behavior, especially move operations in containers. It is not a universal speed boost.
3. Forgetting that noexcept changes failure behavior
Broken example:
void update() noexcept {
riskyOperation();
}
If riskyOperation() throws, the program ends.
Avoid this unless that is exactly what you want.
4. Not using conditional noexcept in templates
Broken example:
template <typename T>
void process(T& value) noexcept {
T copy = value; // copy may throw
}
Better:
template <typename T>
void process(T& value) noexcept(noexcept(T(value))) {
T copy = value;
}
5. Ignoring move constructors and move assignment
If your type is movable and meant for standard containers, this is one of the first places to check.
class Data {
public:
Data(Data&&) noexcept = default;
Data& operator=(Data&&) noexcept = default;
};
6. Overusing noexcept in application-level logic
Business logic often needs to report failure naturally. Overusing noexcept can make code more rigid and less maintainable.
Comparisons
| Concept | Meaning | If exception escapes | Common use |
|---|---|---|---|
| No specification | Function may throw | Normal exception propagation | General-purpose functions |
noexcept | Function promises not to let exceptions escape | std::terminate() | Move ops, swap, accessors, low-level utilities |
noexcept(condition) | Promise depends on a compile-time condition | std::terminate() when declared non-throwing | Templates and generic code |
noexcept vs old-style throw()
| Feature | throw() | noexcept |
|---|---|---|
| Modern C++ use | Deprecated/removed | Preferred |
| Meaning | Old exception specification | Non-throwing guarantee |
| Tooling/library support | Legacy | Standard modern practice |
noexcept vs not writing it
| Choice | Best when |
|---|---|
Use noexcept | The function truly should never fail by exception, and this guarantee matters to callers or generic code |
| Leave it off | The function may throw now or in the future, or the guarantee adds little value |
Where noexcept matters most
| Function type | Usually use noexcept? | Why |
|---|---|---|
| Destructor | Yes, effectively non-throwing | Throwing is dangerous during unwinding |
| Move constructor | Often yes | Helps containers move safely |
| Move assignment | Often yes | Same reason |
swap | Often yes | Generic algorithms prefer non-throwing swap |
| Getter/accessor | Often yes if truly trivial | Clear API contract |
| Parser / file loader / network call | Usually no | Failure is expected and must be reported |
Cheat Sheet
Quick rules
noexceptmeans: no exception may escape this function.- If an exception does escape, the program calls
std::terminate(). - Use it when non-throwing behavior is part of the function contract.
- Most valuable for move operations,
swap, destructors, and trivial accessors. - Do not add it to functions that may reasonably need to throw.
Syntax
void f() noexcept;
void g() noexcept(true);
void h() noexcept(false);
template <typename T>
void func() noexcept(noexcept(T{}));
Best candidates
class A {
public:
A(A&&) noexcept = default;
A& operator=(A&&) noexcept = default;
int value() const noexcept { return v; }
void swap(A& other) noexcept {
std::swap(v, other.v);
}
private:
int v{};
};
Avoid using noexcept when
- the function performs I/O and may fail
- it parses or validates input and may reject data
- it allocates memory in ways you do not want to treat as fatal
- the implementation may evolve to throw later
Important effect
For containers like std::vector:
noexceptmove constructor -> vector can move elements during reallocation- potentially throwing move constructor -> vector may copy instead
Expression check
noexcept(expression)
This returns whether the expression is known not to throw.
Example:
static_assert(noexcept(std::declval<int&>() = 5));
Simple decision guide
- Is this operation conceptually unable to fail? -> consider
noexcept - Will generic code or containers benefit from knowing that? -> strongly consider
noexcept - Could callers reasonably expect exceptions from this function? -> leave it off
FAQ
What does noexcept do in C++?
It declares that a function will not let exceptions escape. If one does, the program terminates instead of propagating the exception.
Should I mark every non-throwing function noexcept?
No. Focus on functions where the guarantee matters: move operations, swap, destructors, and simple accessors. For many ordinary functions, leaving it off is fine.
Does noexcept improve performance?
Sometimes, but usually indirectly. The most important real effect is that standard containers may use move operations more aggressively when those moves are noexcept.
Why does std::vector care about noexcept?
During reallocation, vector wants to preserve exception safety. If moving elements might throw, it may choose copying instead. If moving is noexcept, moving becomes safer to use.
Can a noexcept function still contain code that throws?
Yes. But if an exception escapes the function, std::terminate() is called.
Is noexcept checked at compile time?
The declaration is checked syntactically, but the compiler does not prove all code paths are non-throwing in the general case. You are making a promise.
Should getters be noexcept?
Usually yes, if they simply return stored values and cannot throw.
Should constructors be noexcept?
Only when construction truly cannot fail. Many constructors allocate memory or validate inputs, so they should not automatically be marked noexcept.
Mini Project
Description
Build a small movable C++ type and store it in a std::vector to observe how noexcept affects container behavior during reallocation. This project demonstrates one of the most practical reasons to use noexcept in everyday C++: enabling efficient and safe moves.
Goal
Create a class with copy and move operations, push objects into a vector, and observe when moves are preferred because the move constructor is marked noexcept.
Requirements
- Create a class that prints when it is copied or moved.
- Store objects of that class in a
std::vector. - Trigger vector reallocation by pushing multiple elements.
- Mark the move constructor and move assignment operator with
noexcept. - Compare the behavior with and without
noexcepton the move operations.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.