Docs

README

Rule of 3/5/0, Move Semantics & RAII

Table of Contents

  1. The Rule of Three
  2. The Rule of Five
  3. The Rule of Zero
  4. Move Semantics
  5. RAII Pattern
  6. 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

ResourceRAII Wrapper
Raw pointerstd::unique_ptr, std::shared_ptr
Arraystd::vector, std::array
C stringstd::string
Filestd::fstream, std::ifstream, std::ofstream
Mutex lockstd::lock_guard, std::unique_lock, std::scoped_lock
Threadstd::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)
README - C++ Tutorial | DeepML