C++ Rule of Three, Five and Zero: Safe Object Copying and Moving
Follow one four-integer Buffer from a dangerous shallow copy to correct copy and move operations, then remove the lifetime code with std::vector.
KnowledgeGate Team
Exam prep & CS education

Your C++ class may work until an object is copied, assigned, moved, or destroyed. With a raw owning pointer, two objects can claim one allocation, causing shared mutations, leaks, or a double delete[]. A copyable Buffer initialized with [3, 1, 4, 1] demonstrates all three rules.
C++ Rule of Three, Five and Zero starts with ownership
Our first Buffer has std::size_t size_ and an owning int* data_. Its invariant is size_ == 0 exactly when data_ == nullptr; otherwise, data_ points to one owned new int[size_] allocation. Revise pointers, classes, and constructors in the C++ Tutorial if needed.
Rule | Functions declared by the class | Purpose |
|---|---|---|
Three |
| Release and deep-copy the resource |
Five | Those three, plus | Transfer ownership |
Zero | None of the five | Delegate ownership to an RAII member |
A constructor creates an object; assignment replaces an already-live object's value. First decide whether the type is a copyable value, move-only owner, or non-owning view. Class-policy questions ask whether destruction, deep-copy construction, and exception-safe assignment preserve one resource per owner. Expression-category questions ask which overload binds and whether elision occurs; C++ Move Semantics Tutorial: Trace Ownership with Runnable Examples works through those traces and vector relocation.
C++ shallow copying makes one allocation look like two resources
This constructor establishes the invariant and copies only when the list is non-empty:
Buffer(std::initializer_list<int> values)
: size_(values.size()),
data_(size_ ? new int[size_] : nullptr) {
if (size_ > 0) {
std::copy(values.begin(), values.end(), data_);
}
}Construct Buffer a{3, 1, 4, 1}. Its size_ is 4; suppose its illustrative data_ address is 0x1000. Compiler-generated Buffer b = a copies 4 and 0x1000, not the array. Thus a.data_ == b.data_, with one allocation.
Execute b[1] = 8. Both objects now observe [3, 8, 4, 1], proving that this supposed value copy aliases storage. If ~Buffer() calls delete[] data_, the first destructor frees 0x1000; the second tries to free 0x1000 again. That second deletion, and any access after the first deletion, is undefined behaviour, not a guaranteed crash.

C++ Rule of Three implements a real deep copy
The Rule of Three gives each copied value its own allocation:
~Buffer() {
delete[] data_;
}
Buffer(const Buffer& other)
: size_(other.size_),
data_(size_ > 0 ? new int[size_] : nullptr) {
if (size_ > 0) {
std::copy_n(other.data_, size_, data_);
}
}
Buffer& operator=(const Buffer& other) {
if (this == &other) {
return *this;
}
Buffer temp(other);
std::swap(size_, temp.size_);
std::swap(data_, temp.data_);
return *this;
}Before copying, a owns 0x1000 -> [3, 1, 4, 1]. Buffer b = a allocates one new array, 0x2000 -> [3, 1, 4, 1]. After b[1] = 8, a remains [3, 1, 4, 1] while b becomes [3, 8, 4, 1]. Two allocations allow two valid deletions.
After copy-and-swap, temp owns the destination's old resource and releases it once. If constructing temp fails, the destination is unchanged, giving the strong exception guarantee. Deleting first could lose the old value before the next allocation succeeds. This connects value semantics to object-oriented class design.
C++ Rule of Five transfers ownership during moves
The Rule of Five transfers the pointer instead of copying four elements:
Buffer(Buffer&& other) noexcept
: size_(std::exchange(other.size_, 0)),
data_(std::exchange(other.data_, nullptr)) {}
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
size_ = std::exchange(other.size_, 0);
data_ = std::exchange(other.data_, nullptr);
}
return *this;
}Start with b owning 0x2000 -> [3, 8, 4, 1]. Buffer c = std::move(b) makes zero array allocations, transfers 0x2000 to c, and leaves this implementation's b as {size_=0, data_=nullptr}. std::move is only a cast that permits the move overload.
Let d own 0x3000 -> [6, 2]. d = std::move(c) deletes 0x3000 once, transfers 0x2000 -> [3, 8, 4, 1] to d, and leaves c empty. Moved-from objects stay valid, but empty is this class's chosen state, not a universal promise.

C++ special-member traps that survive copied boilerplate
A raw-owning class should not trust implicit special members. Memberwise copy can shallow-copy its pointer, while a user-declared destructor prevents automatic move generation. Define the complete policy, or disable copying with = delete for a move-only type.
Four defects deserve a direct check:
delete data_is wrong for an array. Usedelete[] data_.Overwriting a destination pointer during copy assignment leaks its old array. Copy-and-swap releases it.
Stealing a pointer without nulling the source causes a later double deletion.
A potentially throwing move must not be
noexcept. Truthfulnoexceptlets containers such asstd::vectorrelocate by moving.
Copying an empty buffer must produce {0, nullptr} without pointer arithmetic on nullptr. a = a preserves [3, 1, 4, 1]. The identity guard makes self move-assignment preserve its value. Every allocation is released exactly once.
C++ Rule of Zero replaces manual ownership with std::vector
The safer design delegates ownership to an RAII member:
class Buffer {
std::vector<int> data_;
public:
Buffer(std::initializer_list<int> values) : data_(values) {}
int& operator[](std::size_t index) { return data_[index]; }
const int& operator[](std::size_t index) const { return data_[index]; }
};This class declares none of the five special members. std::vector owns its storage, so generated copies perform a deep copy. After Buffer a{3, 1, 4, 1}; Buffer b = a; b[1] = 8;, a remains [3, 1, 4, 1] and b becomes [3, 8, 4, 1].
Buffer c = std::move(b) may transfer vector storage. Afterward, b is valid but unspecified, not necessarily empty. Use std::vector<int> for deep-copy values, std::unique_ptr<int[]> for unique ownership, and std::span<int> for a view whose source outlives it. Practise these ownership choices with containers in Coding & DSA Courses for Placements.
C++ Rule of Three, Five and Zero in exams and interviews
Trace which special member runs, identify aliasing or double deletion, count allocations, justify noexcept, and choose an owning member. Revise class mechanics with OOP for Teaching CS Exams: Classes and Inheritance.
Apply three quick checks:
Broken raw-pointer
Buffer y = xcreates zero arrays and two owners, so destruction is unsafe.Explicit-move
Buffer y = std::move(x)creates zero arrays, transfers the pointer, and leaves this implementation'sxnull.Vector-backed
Buffer y = x; y[0] = 9;changes onlyy. Ifxbegan[3, 1, 4, 1], its first element remains3.
In an interview, state the ownership invariant and copy and move contracts, then explain why the library member removes handwritten lifetime code.
C++ Rule of Zero is the short version and the safer next step
No owned resource means do not declare special members.
Manual resource ownership plus value copying means implement all five correctly.
An RAII member such as
std::vectorusually returns the class to the Rule of Zero.
The first two rules explain the mechanics, while the Rule of Zero is usually the production target. For structured work through C++ fundamentals, object-oriented programming, and practice, continue with C++ Programming.
Keep learning

C++ Object Lifetime and Storage Duration: Trace Construction, Destruction, and Dangling State
Learn to separate scope, storage duration, lifetime, and ownership. Trace seven objects precisely, then repair dangling pointers, references, and views.

C++ Diamond Problem: Ambiguity, Duplicate State and Virtual Inheritance
See why a C++ diamond duplicates base state, how virtual inheritance shares it, and when qualification, interfaces, or composition is the clearer design.

C++ Abstract Classes and Interfaces: Build a Usable Storage Contract
Build a C++17 ScoreStore interface, swap vector and hash-backed implementations behind it, and verify both with the same runnable contract test.

C++ Virtual Functions and Polymorphism: Trace Dynamic Dispatch Step by Step
Trace a Shape hierarchy line by line to see how static type, dynamic type, final overriders, object slicing, and virtual destructors determine C++ output.