Skip to content

📜 Rule of 5 — Special Member Functions

Nếu class quản lý resource, bạn cần định nghĩa 5 special member functions hoặc = default / = delete tất cả.

The 5 Special Members

cpp
class Resource {
public:
    // 1. Destructor
    ~Resource();
    
    // 2. Copy Constructor
    Resource(const Resource& other);
    
    // 3. Copy Assignment
    Resource& operator=(const Resource& other);
    
    // 4. Move Constructor
    Resource(Resource&& other) noexcept;
    
    // 5. Move Assignment
    Resource& operator=(Resource&& other) noexcept;
};

Analogy: Hợp đồng chuyển nhượng

┌─────────────────────────────────────────────────────────────────┐
│                    CONTRACT ANALOGY                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   Class = Công ty sở hữu tài sản                               │
│                                                                 │
│   1. Destructor = Giải thể công ty                             │
│      → Phải thanh lý tài sản                                    │
│                                                                 │
│   2. Copy Constructor = Thành lập công ty mới từ mẫu           │
│      → Sao chép tất cả tài sản                                  │
│                                                                 │
│   3. Copy Assignment = Sáp nhập công ty                        │
│      → Hủy tài sản cũ, sao chép tài sản mới                    │
│                                                                 │
│   4. Move Constructor = Thành lập mới, chuyển nhượng tài sản   │
│      → Công ty cũ rỗng, công ty mới có tất cả                  │
│                                                                 │
│   5. Move Assignment = Tiếp quản hoàn toàn                     │
│      → Hủy của mình, lấy của họ, họ rỗng                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Why Rule of 5?

If your class manages a resource (memory, file handle, socket, etc.), the compiler-generated defaults WON'T work correctly.

cpp
// ❌ BAD: Uses default copy (shallow copy)
class BadBuffer {
    int* data_;
    size_t size_;
    
public:
    BadBuffer(size_t size) : data_(new int[size]), size_(size) {}
    ~BadBuffer() { delete[] data_; }
    
    // Compiler generates shallow copy!
};

int main() {
    BadBuffer b1(100);
    BadBuffer b2 = b1;  // Shallow copy: b2.data_ == b1.data_!
    
}  // 💥 Double delete! b1 and b2 both delete same memory!

Complete Example

cpp
#include <algorithm>  // std::copy
#include <utility>    // std::exchange

class Buffer {
    int* data_;
    size_t size_;
    
public:
    // Constructor
    explicit Buffer(size_t size) 
        : data_(new int[size])
        , size_(size) 
    {}
    
    // 1. Destructor
    ~Buffer() {
        delete[] data_;
    }
    
    // 2. Copy Constructor (deep copy)
    Buffer(const Buffer& other) 
        : data_(new int[other.size_])
        , size_(other.size_) 
    {
        std::copy(other.data_, other.data_ + size_, data_);
    }
    
    // 3. Copy Assignment
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {  // Self-assignment check
            delete[] data_;    // Free old resource
            
            size_ = other.size_;
            data_ = new int[size_];
            std::copy(other.data_, other.data_ + size_, data_);
        }
        return *this;
    }
    
    // 4. Move Constructor
    Buffer(Buffer&& other) noexcept
        : data_(std::exchange(other.data_, nullptr))
        , size_(std::exchange(other.size_, 0))
    {}
    
    // 5. Move Assignment
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;  // Free old resource
            
            data_ = std::exchange(other.data_, nullptr);
            size_ = std::exchange(other.size_, 0);
        }
        return *this;
    }
    
    // Accessors
    size_t size() const { return size_; }
    int& operator[](size_t i) { return data_[i]; }
};

Copy vs Move

┌─────────────────────────────────────────────────────────────────┐
│                    COPY vs MOVE                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   COPY (expensive)                 MOVE (cheap)                 │
│   ────────────────                 ────────────                 │
│                                                                 │
│   Source        Dest               Source        Dest           │
│   ┌─────┐      ┌─────┐            ┌─────┐      ┌─────┐         │
│   │ ABC │ ───► │ ABC │            │ ABC │ ───► │ ABC │         │
│   └─────┘      └─────┘            └──┬──┘      └──┬──┘         │
│       ↓           ↓                  ↓ swap       ↓             │
│   ┌─────┐      ┌─────┐            ┌─────┐      ┌─────┐         │
│   │ ABC │      │ ABC │            │null │      │ ABC │         │
│   └─────┘      └─────┘            └─────┘      └─────┘         │
│                                                                 │
│   Duplicates data               Just transfers pointer          │
│   O(n) for n bytes             O(1) constant time               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Copy-and-Swap Idiom

A elegant, exception-safe way to implement assignment:

cpp
class Buffer {
    // ... same as before ...
    
    // Single unified assignment operator
    Buffer& operator=(Buffer other) noexcept {  // By value!
        swap(*this, other);
        return *this;
    }  // other destroyed here, cleans up old data
    
    friend void swap(Buffer& a, Buffer& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }
};

Rule of Zero

💡 BEST PRACTICE

Rule of Zero: If your class doesn't manage resources directly, don't define any of the 5.

Use smart pointers instead!

cpp
// ✅ Rule of Zero - use smart pointers
class ModernBuffer {
    std::unique_ptr<int[]> data_;
    size_t size_;
    
public:
    explicit ModernBuffer(size_t size) 
        : data_(std::make_unique<int[]>(size))
        , size_(size) 
    {}
    
    // No destructor needed - unique_ptr handles it
    // No copy constructor - unique_ptr is move-only
    // No copy assignment - unique_ptr is move-only
    // Move constructor - compiler generates correctly!
    // Move assignment - compiler generates correctly!
};

Rule of 5 Defaults

cpp
class Widget {
public:
    // Explicitly use compiler defaults
    ~Widget() = default;
    Widget(const Widget&) = default;
    Widget& operator=(const Widget&) = default;
    Widget(Widget&&) = default;
    Widget& operator=(Widget&&) = default;
};

Disable Operations

cpp
class NonCopyable {
public:
    NonCopyable() = default;
    ~NonCopyable() = default;
    
    // Disable copy
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
    
    // Enable move
    NonCopyable(NonCopyable&&) = default;
    NonCopyable& operator=(NonCopyable&&) = default;
};

Summary Table

FunctionWhen Called
DestructorObject goes out of scope
Copy ConstructorT obj2(obj1); or T obj2 = obj1;
Copy Assignmentobj2 = obj1; (obj2 exists)
Move ConstructorT obj2(std::move(obj1));
Move Assignmentobj2 = std::move(obj1);

📚 Tổng kết

RuleDescription
Rule of 5Define all 5 or none
Rule of ZeroUse smart pointers, define none
noexceptMark move operations noexcept
Self-assignmentCheck this != &other

🎯 Module Complete!

Bạn đã hoàn thành Module 9: Memory Management:

  • ✅ new/delete dangers
  • ✅ RAII principle
  • ✅ unique_ptr (default choice)
  • ✅ shared_ptr (shared ownership)
  • ✅ weak_ptr (breaking cycles)
  • ✅ Rule of 5

Welcome to Modern C++! 🧠