Recommended resources
The freely available resources this lesson will direct you to.
Hello! Last lesson distinguished copying a value from moving a unique owner and copying a shared owner. Now we will make those operations precise for a class that owns a resource. The central question is: when an object is copied or moved, what happens to the resource it owns?
You will compare the Rule of Zero with the Rule of Five, then implement and inspect a small owning buffer. Allow about 40–45 minutes, including the selected video and reading.
A raw handle cannot explain its ownership to the compiler
Imagine a buffer that stores a pointer to an allocated array:
class Buffer {
std::byte* data_;
std::size_t size_;
public:
explicit Buffer(std::size_t size)
: data_(new std::byte[size]{}), size_(size) {}
~Buffer() { delete[] data_; }
};
The destructor releases the array, but the class is not yet safe to copy. A compiler-generated copy constructor copies data_ as a pointer value. Both objects then believe they own the same array, and both destructors attempt to release it. Compiler-generated copy assignment has an additional failure: it overwrites the destination’s pointer, leaking the array that destination previously owned.
A move is not automatically the remedy. Moving a raw pointer as a member also just copies its address; it does not clear the source pointer. Moreover, declaring a destructor prevents the compiler from implicitly declaring move operations for this class. An apparent “move” might instead call a copy operation.
Watch Andre Kostur’s Back to Basics: The Rule of Five in C++ on CppCon for the sequence of failures, rather than treating the rule as a list to memorize:
Back to Basics: The Rule of Five in C++ - Andre Kostur - CppCon 2023
Watch the string example develop from a destructor into correct copy and move operations. It makes the distinct jobs of construction and assignment visible.
In copying failures, focus on why copy construction causes shared ownership of one allocation, while copy assignment can also leak the destination's old allocation. In moving ownership, watch for the explicit step that leaves the source unable to release the transferred allocation.
The C++ Core Guidelines terminology, summarized by cppreference, calls the need to coordinate destructor and copy operations the Rule of Three. With move construction and move assignment added, the design decision concerns five operations. “Rule of Five” does not mean every class must contain five handwritten function bodies. It means that once you customize or delete one of these ownership operations, you should deliberately decide what each of the others must do.
The rule of three/five/zero - cppreference.com
Read cppreference's explanation to connect the observed failures to the Rule of Three and the alternative design offered by the Rule of Zero.
Under “Rule of three,” read the opening explanation, from its first paragraph through the paragraph describing a shallow copy of a resource handle. Then, under “Rule of zero,” read the design principle and continue through the rule_of_zero string example. Stop before the paragraph beginning “When a base class is intended for polymorphic use.” Focus on why the member type changes what the compiler-generated operations mean.
Prefer the Rule of Zero when members already know what to do
For an ordinary owned byte sequence, std::vector already implements allocation, destruction, deep copying, and ownership transfer:
#include <cstddef>
#include <vector>
class MessageBuffer {
std::vector<std::byte> bytes_;
public:
explicit MessageBuffer(std::size_t size) : bytes_(size) {}
std::size_t size() const noexcept { return bytes_.size(); }
};
MessageBuffer declares none of the five ownership operations. Its compiler-generated copy constructor copies bytes_, so each buffer owns its own elements. Its generated move constructor moves bytes_, letting the vector manage transfer according to its own rules. Destruction and assignment likewise follow the vector member. This is the Rule of Zero: arrange for ownership-aware members to do the work, rather than reimplementing it in the containing class.
The choice of member also determines whether copying is allowed. A class whose sole owning member is a std::unique_ptr can be move-only without implementing cleanup or transfer itself. Its copy operations are unavailable because copying that member is unavailable. If the class must support deep copying despite holding a unique_ptr, you need custom copy operations; declaring those suppresses implicit move operations, so you would explicitly request or implement moves as well.
Watch the later part of Kostur’s talk for this transition from a handwritten owner to ownership-aware members:
Back to Basics: The Rule of Five in C++ - Andre Kostur - CppCon 2023
Return to the talk to see both the benefit and the limit of delegating ownership to a member.
In owning members, notice that unique_ptr handles cleanup and transfer but cannot provide the deep-copy behavior wanted for the example string; after custom copy operations are added, its move operations must be explicitly restored. In Rule of Zero, compare the simpler std::string-member design.
The supplied special-member chart is a useful reminder of which declarations affect the others:

Read the chart as a declaration guide, not a correctness certificate. A generated operation acts on the members it has: copying or moving a raw pointer does not duplicate or transfer its allocation. A defaulted operation can also be unusable if a member cannot perform that operation. The critical question remains whether the resulting behavior preserves your class’s ownership invariant.
Implement the Rule of Five when you manage the allocation yourself
Sometimes a type really does need direct control over a resource. The following ByteBuffer owns exactly one array, or owns no array when empty. Its invariant is:
- If
size_is zero,data_isnullptr. - Otherwise,
data_points to an array ofsize_bytes owned exclusively by this object.
Copying must allocate an independent array. Moving must transfer the array and leave the source empty. Assignment must also account for the destination’s existing allocation.
#include <algorithm>
#include <cstddef>
#include <utility>
class ByteBuffer {
std::byte* data_ = nullptr;
std::size_t size_ = 0;
public:
explicit ByteBuffer(std::size_t size)
: data_(size ? new std::byte[size]{} : nullptr),
size_(size) {}
~ByteBuffer() {
delete[] data_;
}
ByteBuffer(const ByteBuffer& other)
: ByteBuffer(other.size_) {
if (size_ != 0) {
std::copy_n(other.data_, size_, data_);
}
}
ByteBuffer& operator=(const ByteBuffer& other) {
ByteBuffer temporary(other); // Copy before changing *this.
swap(temporary); // Temporary later releases old data.
return *this;
}
ByteBuffer(ByteBuffer&& other) noexcept
: data_(std::exchange(other.data_, nullptr)),
size_(std::exchange(other.size_, 0)) {}
ByteBuffer& operator=(ByteBuffer&& other) noexcept {
if (this != &other) {
delete[] data_; // Release the destination's old array.
data_ = std::exchange(other.data_, nullptr);
size_ = std::exchange(other.size_, 0);
}
return *this;
}
void swap(ByteBuffer& other) noexcept {
std::swap(data_, other.data_);
std::swap(size_, other.size_);
}
std::byte* data() noexcept { return data_; }
const std::byte* data() const noexcept { return data_; }
std::size_t size() const noexcept { return size_; }
};
Trace copy assignment carefully. First, temporary obtains its own copy of other’s bytes. If allocation fails, the destination has not changed. Once that succeeds, swap gives the destination the new array; the temporary takes the destination’s old array and releases it on destruction. This also works for buffer = buffer: the copy is independent, so no special self-copy branch is needed. The approach is simple, though it allocates even when existing destination storage might have been reusable.
Move construction has no old destination resource to release. It takes both pointer and size, setting the source to the documented empty state. Move assignment does have an old destination resource, so it releases that first; its self-move check avoids releasing the very array it would then try to take. Both move operations are noexcept because, for this implementation, they only manipulate these members and release the old array. A non-throwing move matters to containers such as std::vector, which may choose copying over a potentially throwing move when relocating elements.
You can inspect the intended behavior with a small caller:
#include <cassert>
#include <utility>
ByteBuffer original{64};
original.data()[0] = std::byte{0x2a};
ByteBuffer copied = original;
assert(copied.data() != original.data());
copied.data()[0] = std::byte{0x01};
assert(original.data()[0] == std::byte{0x2a});
ByteBuffer moved = std::move(original);
assert(original.data() == nullptr && original.size() == 0);
copied = copied; // Valid self-copy.
moved = std::move(copied); // Releases moved's old array, then takes copied's.
assert(copied.data() == nullptr && copied.size() == 0);
Here, std::move(original) permits selection of a move constructor; it does not transfer anything by itself. The constructor performs the transfer. Nor should you assume every class leaves a moved-from object empty: that is a guarantee this implementation provides. More generally, a moved-from object must remain valid for destruction and whatever other operations its type promises.
A raw-pointer implementation is valuable to understand and sometimes necessary, but it creates a maintenance obligation: every new owning field must be considered in all relevant special members. For a byte sequence with no additional requirements, the earlier std::vector design is the better starting point.
Conclusion
Use the Rule of Zero when ownership-aware members can supply the copy, move, and cleanup behavior you want. If you directly manage a resource, decide all five operations together: destruction releases it; copying creates independent ownership or is deleted; moving transfers ownership or is deleted; and assignment handles the destination’s existing resource. Check the class invariant after each operation, especially in the moved-from object.
The next lesson examines value categories and reference overload resolution. That will make it easier to predict which copy or move operation a call selects—before reasoning about what that operation does.