C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
100: noexcept and Its Guarantees
Imagine you're hiring a contractor to renovate your kitchen. Normally, contractors leave a bit of wiggle room in the contract: "I'll do the job, but if I find mold in the walls or a burst pipe, I'm going to stop work and call you to figure out a new plan." That's how most C++ functions work—they might finish the job, or they might throw an exception that you (the caller) have to handle.
Now, imagine a "Zero-Surprise Guarantee" contract. The contractor promises that under no circumstances will they stop work or ask for more money. They guarantee the job will be completed exactly as described. But there's a catch: if they accidentally break a pipe despite that guarantee, they don't call you to negotiate. They just walk off the job and the whole project is immediately declared a total loss. No recovery, no fixing it—just total failure.
In C++, noexcept is that "Zero-Surprise Guarantee." When you mark a function as noexcept, you aren't just telling the programmer "I don't plan to throw anything"; you are telling the compiler, "If an exception ever escapes this function, just kill the whole program immediately."
The Promise You Can't Break
When you add noexcept to a function signature, you're creating a hard contract. If the function throws an exception that isn't caught inside the function body, the runtime calls std::terminate(). It doesn't look for a catch block further up the stack. It just ends.
struct DataBuffer {
int* data;
// I'm promising this will never throw.
~DataBuffer() noexcept {
delete[] data;
}
void clear() noexcept {
// If I accidentally called something here that threw an exception,
// the program would crash instantly.
data = nullptr;
}
};
I'll be honest: beginners often think, "Why would I want my program to crash instead of letting me catch the error?" The answer isn't about error handling—it's about performance and the "Strong Exception Guarantee."
Why the Standard Library Cares
This is where noexcept actually becomes a tool for speed. Let's look at std::vector. When a vector needs to grow, it allocates a new, larger block of memory and moves the existing elements from the old block to the new one.
Here is the dilemma the vector faces: if it's moving 100 elements and the 50th element's move constructor throws an exception, the vector is in a nightmare state. Half the elements are moved, half are still in the old spot, and the original state is destroyed. It can't easily "roll back" the move.
To solve this, std::vector checks if your move constructor is marked noexcept. If it is, the vector uses the fast move. If it isn't, the vector plays it safe and copies every single element instead. If a copy throws, the vector can just discard the new memory and the original data remains untouched. I've seen developers wonder why their "efficient" move constructors weren't being called, only to realize they forgot that one little keyword.
class HeavyObject {
std::string name;
int* large_array;
public:
// Without noexcept, std::vector will COPY this object during resize
HeavyObject(HeavyObject&& other) noexcept {
this->large_array = other.large_array;
other.large_array = nullptr;
}
};
Conditional Guarantees
Sometimes, you don't know if a function will throw until you know what's happening inside a template. C++ gives us a way to make the noexcept specifier conditional. You can pass a boolean expression into noexcept().
If the expression inside the parentheses evaluates to true, the function is treated as noexcept. If false, it's treated as a normal, potentially-throwing function.
template <typename T>
void swap_values(T& a, T& b) noexcept(noexcept(a.swap(b))) {
a.swap(b);
}
Read that carefully: noexcept(noexcept(a.swap(b))). The inner noexcept(...) is actually an operator that asks the compiler, "Would this expression throw?" The outer noexcept then applies that answer to the function itself. It's a bit meta, but it's the only way to write generic library code that remains performant across different types.
📋 Practical Task
Exercise: Optimizing a Custom Buffer for Vector Resizing
You have a class called SmartBuffer that manages a heap-allocated array. Currently, when this class is stored in a std::vector, the vector is performing expensive copies during resizing because it doesn't trust the move constructor.
Your Task:
- Implement a move constructor for
SmartBufferthat transfers ownership of the pointer. - Apply the correct
noexceptspecifier to ensurestd::vectorutilizes the move constructor instead of falling back to the copy constructor. - Implement a move assignment operator, also marked
noexcept.
#include <iostream>
#include <vector>
#include <string>
class SmartBuffer {
size_t size;
int* buffer;
public:
SmartBuffer(size_t s) : size(s), buffer(new int[s]) {
std::cout < "Allocating buffer of size " < size < "\n";
}
~SmartBuffer() {
delete[] buffer;
}
// Copy constructor (keep this as is)
SmartBuffer(const SmartBuffer& other) : size(other.size), buffer(new int[other.size]) {
std::cout < "Copying buffer...\n";
for (size_t i = 0; i < size; ++i) buffer[i] = other.buffer[i];
}
// TODO: Implement the move constructor here.
// Make sure it is marked so std::vector will use it!
// TODO: Implement the move assignment operator here.
// Make sure it is marked so std::vector will use it!
};
int main() {
std::vector<SmartBuffer> vec;
vec.reserve(1);
vec.emplace_back(10); // First element
std::cout < "Triggering resize...\n";
vec.emplace_back(20); // This should trigger a resize and move the first element
return 0;
}
Success Criteria: When you run the code, you should NOT see "Copying buffer..." printed during the "Triggering resize..." phase. You should only see the allocation messages.
There are no comments for now.