A class with a pointer and size has an invariant: the size must describe the storage the pointer owns. Moving only the pointer while leaving the old size unchanged can break that invariant in the source. Copying the pointer without a suitable ownership policy can create shared deletion responsibility. Lifecycle operations therefore need to preserve the whole representation, rather than merely make a class compile.
This guide uses C++17. You will inspect an owning buffer with coherent copy and move operations, then implement a validate-and-commit update that leaves prior state unchanged on failure. Prefer Rule Zero designs using established value members whenever possible; the manual buffer is a focused demonstration of the responsibilities that custom ownership introduces.
The Rule of Zero avoids unnecessary lifecycle code
A class made from well-behaved value and ownership members can often let the compiler generate its lifecycle operations. Vector, string and unique_ptr already encode important ownership behavior. Choosing those members can remove the need to manually delete raw resources.
Rule Zero does not mean every generated operation is available or matches the domain. A unique_ptr member makes ordinary copying unavailable, while a vector generally supports value copying. If the class's invariant relates several members, a default move might leave their relationship unsuitable for later operations. Review the required copyability and moved-from contract. Use custom special members only when the representation or domain needs them, and keep the complete policy coherent.
Declarations influence special-member generation
Constructors, destructor, copy constructor, copy assignment, move constructor and move assignment participate in generation and suppression rules. A user-declared destructor or copy operation can affect implicit move generation. A type that appears to move under std::move can actually copy if the selected overload is a copy operation.
Std::move is a cast that enables move overload selection; it does not itself transfer a resource. Check the selected type's available operations. Explicitly default or delete appropriate members when that makes the contract clearer. For a resource owner that implements some lifecycle operations manually, review all related operations rather than leaving accidental behavior to a partial set of declarations. The compiler's generated destructor remains suitable in this example because unique_ptr owns cleanup.
Guarantees describe the outcome after failure
The basic guarantee preserves valid invariants and prevents resource leaks, although values may change. The strong guarantee leaves the prior documented state unchanged if the operation fails. The no-throw guarantee promises that the operation does not propagate exceptions. Choose and document the level appropriate to the operation.
Noexcept is a language contract: an exception escaping such a function terminates the program. It is not a request for the compiler to suppress errors. Containers can use nothrow move information when deciding how to relocate elements while preserving their guarantees. Mark a move operation noexcept only when every operation it performs can satisfy that promise under the representation's contract.
Copy-and-swap creates a commit boundary
The copy assignment first constructs a complete candidate. If allocation or copying fails, the target remains untouched. A nonthrowing swap then commits the candidate, and the temporary cleans up the former target state. This structure makes the strong guarantee easy to explain.
The approach can have additional allocation cost and is not the best implementation for every type. The candidate must genuinely represent a valid complete replacement, and swap must satisfy the promised no-throw contract. A hand-written swap that performs throwing work would undermine the argument. Self-assignment handling should also be coherent; the buffer uses an explicit identity check to avoid unnecessary work.
Test operations and state, not keywords
Test independent copying, copy assignment into an existing object, moves, self-assignment under the chosen contract and checked access on the moved-from state. Include assignment between buffers of different sizes to verify that metadata and storage move together. For exception behavior, use controlled failing operations or invalid input to verify the documented state outcome.
A noexcept annotation is not evidence that the body is correct, and successful compilation does not establish resource ownership. Review cleanup, representation invariants and every mutation path. Prefer a smaller value-based representation when a manual owner becomes complicated. The most maintainable lifecycle code is often code made unnecessary by choosing an established owning member.
Official references
C++ Special Members and Exception Guarantees Examples
Each example shows the code, the output it produces and what each part does.
A coherent owning buffer
This complete demonstration uses unique_ptr for cleanup, copies through newly owned storage and resets the moved-from size. Every checked access agrees with the size invariant.
This class specifically promises an empty moved-from source. Do not generalize that promise to every library type, whose moved-from state may only be valid and unspecified.
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <utility>
class Buffer {
std::size_t size_ = 0;
std::unique_ptr<int[]> data_;
public:
explicit Buffer(std::size_t size)
: size_(size),
data_(size ? std::make_unique<int[]>(size) : nullptr) { }
Buffer(const Buffer& other) : Buffer(other.size_) {
if (size_ != 0)
std::copy_n(other.data_.get(), size_, data_.get());
}
Buffer& operator=(const Buffer& other) {
if (this != &other) {
Buffer candidate(other);
swap(candidate);
}
return *this;
}
Buffer(Buffer&& other) noexcept
: size_(std::exchange(other.size_, 0)),
data_(std::move(other.data_)) { }
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
data_ = std::move(other.data_);
size_ = std::exchange(other.size_, 0);
}
return *this;
}
void swap(Buffer& other) noexcept {
std::swap(size_, other.size_);
data_.swap(other.data_);
}
std::size_t size() const noexcept { return size_; }
int& at(std::size_t index) {
if (index >= size_)
throw std::out_of_range("buffer index");
return data_[index];
}
const int& at(std::size_t index) const {
if (index >= size_)
throw std::out_of_range("buffer index");
return data_[index];
}
};
int main() {
Buffer original(2);
original.at(0) = 10;
original.at(1) = 20;
Buffer copied = original;
copied.at(0) = 99;
Buffer moved = std::move(original);
std::cout << "Copy: " << copied.at(0) << '\n';
std::cout << "Moved: " << moved.at(0) << '\n';
std::cout << "Source size: " << original.size() << '\n';
}Copy: 99
Moved: 10
Source size: 0C++ Special Members and Exception Guarantees in Real Projects
Worked exercise: validate before committing a vector update
The task is to append only nonnegative quantities while leaving the original vector unchanged if any new value is invalid. Build the candidate privately, validate and append there, then swap. This complete helper provides a strong state guarantee under the ordinary default-allocator vector contract.
#include <iostream>
#include <stdexcept>
#include <vector>
void append_validated(std::vector<int>& live,
const std::vector<int>& incoming) {
std::vector<int> candidate = live;
for (int value : incoming) {
if (value < 0)
throw std::invalid_argument("negative quantity");
candidate.push_back(value);
}
live.swap(candidate);
}
int main() {
std::vector<int> quantities{2, 4};
try {
append_validated(quantities, {6, -1});
} catch (const std::invalid_argument&) {
std::cout << "Update rejected\n";
}
std::cout << "After rejection: " << quantities.size() << '\n';
append_validated(quantities, {6});
std::cout << "After success: " << quantities.size() << '\n';
}Update rejected
After rejection: 2
After success: 3The valid six in the rejected candidate never reaches live state. Allocation failure during candidate construction or growth also leaves the original untouched. External effects cannot be rolled back merely by swapping a vector, so keep them outside this local guarantee.
C++ Special Members and Exception Guarantees FAQ
Is std::move guaranteed to move an object?
It changes the expression's value category so overload resolution can select an available move operation. If no suitable move exists, another compatible operation can be selected, including copying. The cast itself transfers nothing. Inspect the type's special members and test the relevant ownership outcome rather than treating the presence of std::move as proof of a move.
Must every moved-from object be empty?
No. Standard-library contracts commonly require a valid but unspecified state unless a stronger guarantee is documented. The Buffer example intentionally resets size to zero and promises that state. Other types can behave differently. Do not inspect unspecified details as though they were portable, and design custom types so their moved-from state still satisfies their supported operations and invariants.
What happens if a noexcept function throws?
An exception escaping its noexcept boundary causes termination. The annotation does not catch, repair or ignore the exception. Declare it only when the operation's body and callees support the promise. This matters for move operations and swaps used by containers or commit-style updates. A false promise can convert a recoverable failure into process termination.
Does the strong guarantee undo external effects?
Not by itself. It describes the operation's documented state, and a local candidate-and-swap design can preserve an in-memory value. A network message, payment or file replacement has a separate effect and recovery contract. Keep such effects explicitly coordinated. Do not assume discarding a temporary vector reverses work already completed by another system.
C++ Special Members and Exception Guarantees Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Which step makes the worked update's commit argument sound?
C++ Special Members and Exception Guarantees Quiz
2 questions to check what you learned.
What does the strong exception guarantee promise?
- Every operation must be faster than copying.
- Failure leaves the prior documented state unchanged.
- Every function becomes noexcept automatically.
- External services undo their effects automatically.
Why reset Buffer's moved-from size?
- Because declaring the move noexcept also resets scalar metadata.
- Because std::move resets all integers automatically.
- To keep its size and storage ownership invariant coherent.
- To prevent the destination from owning storage.