Docs
Rule Of Five RAII
Rule of 3/5/0, Move Semantics & RAII
Table of Contents
- •The Rule of Three
- •The Rule of Five
- •The Rule of Zero
- •Move Semantics
- •RAII Pattern
- •Best Practices
The Rule of Three
If a class requires a custom destructor, copy constructor, or copy assignment operator, it likely requires all three.
Why It Matters
// Problem: shallow copy
class BadString {
char* data;
size_t size;
public:
BadString(const char* s) {
size = strlen(s);
data = new char[size + 1];
strcpy(data, s);
}
~BadString() {
delete[] data; // Custom destructor
}
// Default copy constructor does shallow copy!
// BadString(const BadString& other) : data(other.data), size(other.size) {}
};
// PROBLEM:
BadString a("Hello");
BadString b = a; // Shallow copy: both point to same memory!
// When a is destroyed: data is deleted
// When b is destroyed: data is deleted AGAIN! Double free!
Rule of Three Visualization
┌─────────────────────────────────────────────────────────────────────────────┐
│ THE RULE OF THREE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ If you define ANY of these, you probably need ALL THREE: │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Destructor │ │ Copy Constructor│ │ Copy Assignment │ │
│ │ ~ClassName() │ │ ClassName(const │ │ operator=(const │ │
│ │ │ │ ClassName&) │ │ ClassName&) │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ └────────────────────┼────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ Resource Management │ │
│ │ (heap memory, files, │ │
│ │ sockets, handles) │ │
│ └───────────────────────┘ │
│ │
│ Why? │
│ - Destructor: Need to release resource │
│ - Copy Constructor: Need to duplicate resource for new object │
│ - Copy Assignment: Need to release old + duplicate new │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Correct Implementation
class String {
char* data;
size_t size;
public:
// Constructor
String(const char* s = "") {
size = strlen(s);
data = new char[size + 1];
strcpy(data, s);
}
// 1. Destructor
~String() {
delete[] data;
}
// 2. Copy Constructor (deep copy)
String(const String& other) {
size = other.size;
data = new char[size + 1];
strcpy(data, other.data);
}
// 3. Copy Assignment Operator
String& operator=(const String& other) {
if (this != &other) { // Self-assignment check
delete[] data; // Release old resource
size = other.size;
data = new char[size + 1];
strcpy(data, other.data);
}
return *this;
}
const char* c_str() const { return data; }
};
The Rule of Five
C++11 added move semantics. If a class requires the Rule of Three operations, it likely also needs move constructor and move assignment operator.
The Five Special Members
┌─────────────────────────────────────────────────────────────────────────────┐
│ THE RULE OF FIVE (C++11) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ RULE OF THREE (C++98) │ │
│ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │
│ │ │ Destructor │ │ Copy │ │ Copy │ │ │
│ │ │ ~Class() │ │ Constructor │ │ Assignment │ │ │
│ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ + │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ C++11 ADDITIONS │ │
│ │ ┌───────────────────────────┐ ┌───────────────────────────┐ │ │
│ │ │ Move Constructor │ │ Move Assignment │ │ │
│ │ │ Class(Class&& other) │ │ operator=(Class&& other) │ │ │
│ │ └───────────────────────────┘ └───────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ = │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ RULE OF FIVE │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Complete Rule of Five Implementation
class String {
char* data;
size_t size;
public:
// Constructor
String(const char* s = "") {
size = strlen(s);
data = new char[size + 1];
strcpy(data, s);
}
// 1. Destructor
~String() {
delete[] data;
}
// 2. Copy Constructor
String(const String& other) {
size = other.size;
data = new char[size + 1];
strcpy(data, other.data);
}
// 3. Copy Assignment (copy-and-swap idiom)
String& operator=(String other) { // Pass by value = copy
swap(*this, other);
return *this;
} // other is destroyed, releasing old data
// 4. Move Constructor
String(String&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // Leave other in valid state
other.size = 0;
}
// 5. Move Assignment
String& operator=(String&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
// Swap helper (for copy-and-swap)
friend void swap(String& a, String& b) noexcept {
using std::swap;
swap(a.data, b.data);
swap(a.size, b.size);
}
const char* c_str() const { return data; }
size_t length() const { return size; }
};
The Rule of Zero
Prefer classes that don't need custom resource management. Use smart pointers and standard containers instead.
Rule of Zero Philosophy
┌─────────────────────────────────────────────────────────────────────────────┐
│ THE RULE OF ZERO │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ "Classes that don't manage resources shouldn't define │
│ destructor, copy/move constructors, or copy/move assignment operators." │
│ │
│ BEFORE (Rule of Five): AFTER (Rule of Zero): │
│ ┌────────────────────────────┐ ┌────────────────────────────┐ │
│ │ class Person { │ │ class Person { │ │
│ │ char* name; │ │ std::string name; │ │
│ │ int* ages; │ │ std::vector<int> ages; │ │
│ │ │ │ │ │
│ │ ~Person(); │ │ // Nothing needed! │ │
│ │ Person(const Person&); │ │ // Compiler generates │ │
│ │ Person& operator=(...) │ │ // correct versions │ │
│ │ Person(Person&&); │ │ │ │
│ │ Person& operator=(...&&) │ │ }; │ │
│ │ }; │ │ │ │
│ └────────────────────────────┘ └────────────────────────────┘ │
│ │
│ USE RAII WRAPPERS: │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Raw pointer → std::unique_ptr, std::shared_ptr │ │
│ │ Raw array → std::vector, std::array │ │
│ │ C string → std::string │ │
│ │ File handle → std::fstream or custom RAII wrapper │ │
│ │ Mutex lock → std::lock_guard, std::unique_lock │ │
│ │ Database handle → Custom RAII wrapper │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Rule of Zero Example
// Modern C++ - no special members needed
class Person {
std::string name; // Manages its own memory
std::vector<std::string> addresses; // Manages its own memory
std::unique_ptr<Logger> logger; // Manages its own memory
public:
Person(std::string n) : name(std::move(n)) {}
// Rule of Zero: compiler-generated defaults work correctly!
// No destructor needed
// No copy constructor needed (vector copies, unique_ptr moves)
// No copy assignment needed
// No move constructor needed
// No move assignment needed
};
Move Semantics
Move semantics transfer ownership of resources instead of copying them.
lvalue vs rvalue
int x = 42; // x is an lvalue (has identity, persistent)
int y = x + 5; // x + 5 is an rvalue (temporary, no identity)
std::string s = "Hello"; // s is lvalue
std::string t = s + " World"; // s + " World" is rvalue (temporary)
Move Semantics Visualization
┌─────────────────────────────────────────────────────────────────────────────┐
│ COPY vs MOVE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ COPY (expensive): │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ Source │ │ Destination │ │
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │
│ │ │ data ────┼──┼─► [H][e][l][l][o] │ │ data ────┼──┼─► [H][e][l][l][o]│
│ │ └──────────┘ │ (heap) │ └──────────┘ │ (new heap)│
│ │ size = 5 │ │ │ size = 5 │ │ │
│ └────────────────┘ │ └────────────────┘ │ │
│ │ │ │
│ Allocate + Copy data Allocate + Copy│
│ │
│ MOVE (cheap): │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ Source │ │ Destination │ │
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │
│ │ │ data ────┼──┼─► [H][e][l][l][o]◄───┼──┼──── data │ │ │
│ │ └──────────┘ │ (heap) │ └──────────┘ │ │
│ │ size = 5 │ │ size = 5 │ │
│ └────────┬───────┘ └────────────────┘ │
│ │ │
│ ▼ (after move) │
│ ┌────────────────┐ │
│ │ Source │ │
│ │ ┌──────────┐ │ │
│ │ │ data = nullptr │
│ │ └──────────┘ │ │
│ │ size = 0 │ ← "moved-from" state (valid but unspecified) │
│ └────────────────┘ │
│ │
│ Move = Transfer pointer, don't copy data! │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
std::move
std::move casts an lvalue to an rvalue reference, enabling move semantics.
std::string a = "Hello";
std::string b = a; // Copy: b gets copy of "Hello"
std::string c = std::move(a); // Move: c steals "Hello" from a
// a is now in "moved-from" state (valid but empty)
std::vector<std::string> vec;
std::string s = "World";
vec.push_back(s); // Copy into vector
vec.push_back(std::move(s)); // Move into vector (faster!)
// s is now empty
When Move is Called
void foo(std::string s); // By value
std::string a = "test";
foo(a); // Copy (a is lvalue)
foo(std::move(a)); // Move (cast to rvalue)
foo(std::string("temp")); // Move (temporary is rvalue)
foo("literal"); // Move (implicit temporary)
// Return value optimization
std::string bar() {
std::string result = "hello";
return result; // Move (or RVO/NRVO)
}
noexcept and Move
// Move operations should be noexcept when possible
String(String&& other) noexcept; // ✓ Good
String& operator=(String&& other) noexcept; // ✓ Good
// Why? std::vector won't use move if it might throw
// (to maintain strong exception guarantee)
std::vector<String> vec;
vec.push_back(String("test"));
// If move is noexcept, uses move
// If move might throw, uses copy (slower but safe)
RAII Pattern
Resource Acquisition Is Initialization
Core Concept
┌─────────────────────────────────────────────────────────────────────────────┐
│ RAII PATTERN │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ PRINCIPLE: │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Constructor ────► Acquire resource │ │
│ │ Destructor ────► Release resource │ │
│ │ │ │
│ │ Resource lifetime = Object lifetime │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ LIFECYCLE: │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ { │ │
│ │ RAIIWrapper resource("file.txt"); ◄─ Constructor acquires │ │
│ │ resource.use(); │ │
│ │ // ... │ │
│ │ if (error) return; ◄─ Early exit? No problem! │ │
│ │ // ... │ │
│ │ throw exception; ◄─ Exception? No problem! │ │
│ │ } ◄──────────────────────── Destructor releases │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ BENEFITS: │
│ ✓ No manual cleanup needed │
│ ✓ Exception-safe │
│ ✓ No resource leaks │
│ ✓ Clear ownership │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
RAII Examples
// 1. File Handle
class FileHandle {
FILE* file;
public:
FileHandle(const char* path, const char* mode) {
file = fopen(path, mode);
if (!file) throw std::runtime_error("Cannot open file");
}
~FileHandle() {
if (file) fclose(file);
}
// Delete copy (file handles shouldn't be copied)
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// Allow move
FileHandle(FileHandle&& other) noexcept : file(other.file) {
other.file = nullptr;
}
FILE* get() { return file; }
};
// 2. Lock Guard
class LockGuard {
std::mutex& mtx;
public:
LockGuard(std::mutex& m) : mtx(m) { mtx.lock(); }
~LockGuard() { mtx.unlock(); }
LockGuard(const LockGuard&) = delete;
LockGuard& operator=(const LockGuard&) = delete;
};
// 3. Timer (RAII for timing)
class ScopedTimer {
std::chrono::high_resolution_clock::time_point start;
std::string name;
public:
ScopedTimer(std::string n) : name(std::move(n)) {
start = std::chrono::high_resolution_clock::now();
}
~ScopedTimer() {
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration<double, std::milli>(end - start);
std::cout << name << ": " << duration.count() << "ms\n";
}
};
Standard RAII Wrappers
| Resource | RAII Wrapper |
|---|---|
| Raw pointer | std::unique_ptr, std::shared_ptr |
| Array | std::vector, std::array |
| C string | std::string |
| File | std::fstream, std::ifstream, std::ofstream |
| Mutex lock | std::lock_guard, std::unique_lock, std::scoped_lock |
| Thread | std::jthread (C++20), std::thread |
Best Practices
Decision Flowchart
┌─────────────────────────────────────────────────────────────────────────────┐
│ WHICH RULE SHOULD I FOLLOW? │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Does your class manage a resource (memory, file, socket, etc.)? │
│ │
│ NO ──────────► RULE OF ZERO │
│ • Use standard library types │
│ • Let compiler generate defaults │
│ • Example: class with string, vector members │
│ │
│ YES │
│ │ │
│ ▼ │
│ Can you wrap the resource in a standard RAII type? │
│ (unique_ptr, shared_ptr, vector, etc.) │
│ │
│ YES ─────────► RULE OF ZERO (preferred!) │
│ • Use unique_ptr<T> instead of T* │
│ • Let the wrapper handle cleanup │
│ │
│ NO (custom resource, legacy code) │
│ │ │
│ ▼ │
│ RULE OF FIVE │
│ • Define destructor │
│ • Define copy constructor (or = delete) │
│ • Define copy assignment (or = delete) │
│ • Define move constructor (noexcept) │
│ • Define move assignment (noexcept) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Code Examples
// ✓ Good: Rule of Zero
class Employee {
std::string name;
std::unique_ptr<Address> address;
std::vector<std::string> skills;
};
// ✓ Good: Rule of Five (when needed)
class Buffer {
std::byte* data;
size_t size;
public:
Buffer(size_t n) : data(new std::byte[n]), size(n) {}
~Buffer() { delete[] data; }
Buffer(const Buffer& other);
Buffer& operator=(const Buffer& other);
Buffer(Buffer&& other) noexcept;
Buffer& operator=(Buffer&& other) noexcept;
};
// ✓ Good: Non-copyable, movable resource
class UniqueSocket {
int fd;
public:
UniqueSocket(int port);
~UniqueSocket() { if (fd >= 0) close(fd); }
UniqueSocket(const UniqueSocket&) = delete;
UniqueSocket& operator=(const UniqueSocket&) = delete;
UniqueSocket(UniqueSocket&& other) noexcept : fd(other.fd) {
other.fd = -1;
}
UniqueSocket& operator=(UniqueSocket&& other) noexcept;
};
// ✗ Bad: Manual memory management without full Rule of 5
class BadClass {
int* data = new int[100];
public:
~BadClass() { delete[] data; }
// Missing: copy ctor, copy assignment, move ctor, move assignment
// Default copy will cause double-free!
};
Quick Reference
// Rule of Five Template
class Resource {
T* ptr;
public:
// Constructor
Resource() : ptr(new T) {}
// 1. Destructor
~Resource() { delete ptr; }
// 2. Copy Constructor
Resource(const Resource& other) : ptr(new T(*other.ptr)) {}
// 3. Copy Assignment (copy-swap)
Resource& operator=(Resource other) {
swap(*this, other);
return *this;
}
// 4. Move Constructor
Resource(Resource&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
}
// 5. Move Assignment
Resource& operator=(Resource&& other) noexcept {
if (this != &other) {
delete ptr;
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
// Swap helper
friend void swap(Resource& a, Resource& b) noexcept {
using std::swap;
swap(a.ptr, b.ptr);
}
};
// Delete copy, keep move
Resource(const Resource&) = delete;
Resource& operator=(const Resource&) = delete;
// Use defaults explicitly
~Resource() = default;
Resource(const Resource&) = default;
Resource& operator=(const Resource&) = default;
Resource(Resource&&) = default;
Resource& operator=(Resource&&) = default;
Compile & Run
g++ -std=c++17 -Wall -Wextra examples.cpp -o examples
./examples
# Check for move operations
g++ -std=c++17 -fno-elide-constructors examples.cpp -o examples
# (Disables copy elision to see moves)