When a class handles a resource that the compiler can't properly copy or destroy by itself, the five special member functions are no longer optional. They become a set. If you write one of them, you need to write the others (or explicitly delete them). This is the Rule of Five.
Consider a simple class that owns a dynamically allocated integer:
#include <iostream>
class OwnedInt
{
private:
int* m_ptr{};
public:
explicit OwnedInt(int value)
: m_ptr{ new int{ value } }
{
}
// Run this destructor when the object lifetime ends
~OwnedInt()
{
delete m_ptr;
}
int get() const
{
return *m_ptr;
}
};
int main()
{
OwnedInt a{ 42 };
std::cout << a.get() << '\n';
return 0;
}
Constructor: dynamically allocates an int on the heap, and saves the address of it to m_ptr. Destructor: frees this memory using delete. Because we defined a destructor, the move constructors are no longer declared by the compiler.
The copy constructors still are, but they do a "shallow copy": they just copy the pointer value.
Now two OwnedInt instances point to the same place in the heap. When both go out of scope, the same address gets deleted twice. That is undefined behavior.
The Rule of Five states that on any appearance of one of the five special members, five shall be called. The complete set for this would be:
#include <iostream>
#include <utility>
class OwnedInt
{
private:
int* m_ptr{};
public:
explicit OwnedInt(int value)
: m_ptr{ new int{ value } }
{
}
// 1. Destructor
~OwnedInt()
{
delete m_ptr;
}
// 2. Copy constructor
OwnedInt(const OwnedInt& other)
: m_ptr{ new int{ *other.m_ptr } }
{
}
// 3. Copy assignment
OwnedInt& operator=(const OwnedInt& other)
{
if (this == &other)
return *this;
delete m_ptr;
m_ptr = new int{ *other.m_ptr };
return *this;
}
// 4. Move constructor
OwnedInt(OwnedInt&& other) noexcept
: m_ptr{ other.m_ptr }
{
other.m_ptr = nullptr;
}
// 5. Move assignment
OwnedInt& operator=(OwnedInt&& other) noexcept
{
if (this == &other)
return *this;
delete m_ptr;
m_ptr = other.m_ptr;
other.m_ptr = nullptr;
return *this;
}
int get() const
{
return *m_ptr;
}
};
int main()
{
OwnedInt a{ 10 };
OwnedInt b{ a };
OwnedInt c{ std::move(a) };
b = c;
c = OwnedInt{ 99 };
std::cout << b.get() << ' ' << c.get() << '\n';
return 0;
}
Examine each function. The destructor is still cleaning up the resource owned by the current object; the copy constructor allocates a new int and copies the value pointed to by other.m_ptr; the objects are never sharing space. The copy assignment operator follows the same pattern and then cleans up the resource owned by the current object.
The move constructor steals the pointer that already exists inside other. After the steal, it sets other.m_ptr to nullptr, so the destructor for the source object does not free the address twice. The move assignment operator does the same steal, except the destination first releases whatever it was already pointing to. Both move operations are marked noexcept because they never throw: containers and algorithms depend on that fact.
If any one of these five functions is omitted, the class is broken. Remove the move operations and every temporary is forced down the costly copy path. Remove the copy operations and with the destructor and compiler defined moves we have come full circle, shallow pointer copy again giving us double delete. Specify only the destructor and we are neither safely copyable nor safely movable.
The Rule of Five, then, is not a checklist of additional functions to implement arbitrarily.
It is the realization that when those five special members are involved, the class has entered into a cohesive agreement concerning ownership. If a class allocates responsibility for a resource, the contract must be fully specified or otherwise the default behavior of the compiler will undermine it silently.
Exercise: The Rule of FiveStarter code, hints and solutionOpenClose
Practice: The Rule of Five
Work through the mission, reveal hints only when needed, and compare your result with the model solution.
Your challenge
Complete OwnedInt with deep-copy construction/assignment and ownership-transferring move construction/assignment, including self-assignment handling. Exercise all four operations without sharing an owning raw pointer or dereferencing an empty source.
Mission checklist
- Run the starter as a baseline, then predict the change requested in Your challenge.
- Make that change in your editor; use the lesson-specific hints if you need help.
- Compile and run your answer, then compare with the worked solution and explain the result.
Starter program
This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.
#include <iostream>
class OwnedInt
{
private:
int* m_ptr{};
public:
explicit OwnedInt(int value)
: m_ptr{ new int{ value } }
{
}
// Run this destructor when the object lifetime ends
~OwnedInt()
{
delete m_ptr;
}
int get() const
{
return *m_ptr;
}
};
int main()
{
OwnedInt a{ 42 };
std::cout << a.get() << '\n';
return 0;
}Hints
OwnedInt owns m_ptr and deletes it in its destructor, but the starter does not implement the other four special members. Read this as an ownership starting point, not a complete Rule-of-Five class.
Allocate a new int for a copy, but transfer and clear the pointer for a move. In copy assignment, acquire the replacement before deleting the old allocation; handle an empty source and self-assignment.
Worked solution
The completed owner implements all five operations. Copies allocate independent ints; moves transfer the pointer and clear the source. Both assignments handle self-assignment, and get safely handles an empty moved-from owner. The calls exercise copy construction, move construction, copy assignment, and move assignment explicitly.
#include <iostream>
#include <utility>
class OwnedInt
{
int* m_ptr{};
public:
explicit OwnedInt(int value) : m_ptr{new int{value}} {}
~OwnedInt() { delete m_ptr; }
OwnedInt(const OwnedInt& other)
: m_ptr{other.m_ptr ? new int{*other.m_ptr} : nullptr}
{ std::cout << "copy construction\n"; }
OwnedInt& operator=(const OwnedInt& other)
{
if (this != &other) {
int* copy = other.m_ptr ? new int{*other.m_ptr} : nullptr;
delete m_ptr;
m_ptr = copy;
}
std::cout << "copy assignment\n";
return *this;
}
OwnedInt(OwnedInt&& other) noexcept : m_ptr{std::exchange(other.m_ptr, nullptr)}
{ std::cout << "move construction\n"; }
OwnedInt& operator=(OwnedInt&& other) noexcept
{
if (this != &other) {
delete m_ptr;
m_ptr = std::exchange(other.m_ptr, nullptr);
}
std::cout << "move assignment\n";
return *this;
}
bool empty() const { return m_ptr == nullptr; }
int get() const { return m_ptr ? *m_ptr : 0; }
};
int main()
{
OwnedInt a{42};
OwnedInt b{a};
OwnedInt c{std::move(b)};
OwnedInt d{0};
d = a;
OwnedInt e{0};
e = std::move(d);
std::cout << "Values: " << a.get() << ' ' << c.get() << ' ' << e.get() << '\n';
std::cout << "Moved sources empty: " << b.empty() << ' ' << d.empty() << '\n';
}
Example output
copy construction
move construction
copy assignment
move assignment
Values: 42 42 42
Moved sources empty: 1 1
0 comments
No comments yet. Be the first.