5.2.10C++ Programming

Move constructor and move assignment — Rule of Five

1,790 words8 min readdifficulty · medium

WHY does move exist at all?

WHAT is being optimized: copying an O(N) heap buffer is replaced by copying an O(1) pointer.

HOW the compiler knows it's safe: the source is an rvalue (a temporary or something you explicitly cast with std::move). Rvalues have no name you can use again, so stealing from them is safe.


The five special members


Deriving the move constructor from scratch

Let's build a minimal Buffer owning a heap array.

class Buffer {
    int*   data_;
    size_t size_;
public:
    // 0. Constructor
    Buffer(size_t n) : data_(new int[n]{}), size_(n) {}
 
    // 1. Destructor
    ~Buffer() { delete[] data_; }
 
    // 2. Copy constructor — DEEP copy
    Buffer(const Buffer& o) : data_(new int[o.size_]), size_(o.size_) {
        std::copy(o.data_, o.data_ + o.size_, data_);   // Why? source must survive intact
    }
 
    // 3. Copy assignment — copy-and-swap idiom
    Buffer& operator=(const Buffer& o) {
        Buffer tmp(o);        // Why? reuse copy ctor; strong exception safety
        swap(*this, tmp);     // Why? swap is noexcept; tmp's dtor frees our old data
        return *this;
    }
 
    // 4. Move constructor — STEAL
    Buffer(Buffer&& o) noexcept
        : data_(o.data_), size_(o.size_) {  // Why? grab the pointer, no allocation
        o.data_ = nullptr;                  // Why? so o's dtor won't free what we now own
        o.size_ = 0;
    }
 
    // 5. Move assignment — STEAL into existing
    Buffer& operator=(Buffer&& o) noexcept {
        if (this != &o) {            // Why? guard against self-move
            delete[] data_;          // Why? free our current resource first
            data_  = o.data_;        // steal
            size_  = o.size_;
            o.data_ = nullptr;       // leave source in valid empty state
            o.size_ = 0;
        }
        return *this;
    }
 
    friend void swap(Buffer& a, Buffer& b) noexcept {
        std::swap(a.data_, b.data_);
        std::swap(a.size_, b.size_);
    }
};

Figure — Move constructor and move assignment — Rule of Five

Worked examples


Flashcards

What does a move constructor steal and what must it leave behind?
It steals the resource (e.g. pointer) from the source, and must leave the source in a valid, destructible empty state (e.g. set pointer to nullptr).
State the Rule of Five.
If you define any of destructor, copy ctor, or copy assignment, define all five: destructor, copy ctor, copy assignment, move ctor, move assignment.
What does std::move(x) actually do at runtime?
Nothing — it is a compile-time cast of x to an rvalue reference, allowing a move ctor/assignment to be selected. The actual stealing happens inside that move operation.
Why must move operations be marked noexcept?
So standard containers (e.g. std::vector on reallocation) will use the move instead of falling back to a copy to keep the strong exception guarantee.
What bug occurs if you write a destructor but rely on the implicit copy constructor for a pointer member?
Shallow pointer copy → two objects own the same buffer → double free / dangling pointer crash.
Why null out the source in a move?
So the source's destructor does not free the resource that the destination now owns.
What is the signature difference between copy and move constructor?
Copy: T(const T&). Move: T(T&&) noexcept.
Why include a self-assignment check in move assignment?
To avoid deleting your own resource and then reading a dangling pointer when x = std::move(x) happens (often via generic algorithms).

Recall Feynman: explain to a 12-year-old

Imagine you're moving house. Copying = buying all-new furniture identical to the old, then throwing the old away. Wasteful! Moving = the old house is being demolished anyway, so just carry your existing furniture to the new house and leave the old one empty so the wrecking crew doesn't smash your sofa. The "Rule of Five" is the checklist of five chores you must finish so nobody ends up with two families owning the same sofa (which would cause a fight = a crash).

Connections

  • Rule of Three — the pre-C++11 ancestor (D, copy-ctor, copy-assign)
  • Rule of Zero — prefer RAII members so you write none of the five
  • std::move and rvalue references — the cast that enables moves
  • Copy elision and RVO — when even the move is skipped
  • Smart pointers (unique_ptr / shared_ptr) — move-only & RAII resource owners
  • Exception safety guarantees — why noexcept and copy-and-swap matter

Concept Map

requires

motivates

transfers

null source so

makes stealing safe

produces

includes

includes

includes

marked

uses

declaring suppresses

silent fallback to

Manual resource management

Rule of Five

Deep copy problem

Move semantics

Ownership via pointer steal

Destructor skips freeing

Rvalue source

std::move cast

Destructor

Copy ctor and copy assign

Move ctor and move assign

noexcept

copy-and-swap idiom

Implicit move members

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Dekho, jab tum ek bada object (jaise heap pe allocated buffer wali class) ko copy karte ho, toh purana tarika ye tha: naya memory allocate karo, saara data byte-by-byte copy karo — ye O(N) ka mehnga kaam hai. Lekin agar source ek temporary hai jo waise bhi marne wala hai (rvalue), toh itni mehnat kyun? Move ka idea simple hai: source ka pointer cheen lo (steal), aur source ka pointer ko nullptr kar do (sterilize) taaki uska destructor tumhari memory free na kar de. Bas — O(1) mein kaam ho gaya.

Rule of Five kehta hai: agar tumne destructor, copy constructor, ya copy assignment mein se koi ek bhi khud likha, matlab tum manually resource manage kar rahe ho — toh paanchon special members likho: destructor, copy ctor, copy assignment, move ctor, aur move assignment. Agar sirf destructor likha aur baaki compiler pe chhod diya, toh do problem: (1) implicit copy shallow pointer copy karega → double free crash, aur (2) move members generate hi nahi honge, sab kuch slow copy ban jayega.

Ek important baat: std::move(x) khud kuch move nahi karta! Wo bas x ko rvalue reference mein cast karta hai, taaki compiler move version chun sake. Asli "chori" toh move constructor ke andar hoti hai. Aur move operations ko hamesha noexcept mark karo — warna std::vector reallocation ke waqt tumhare move ko ignore karke copy use karega (strong exception guarantee ke liye).

Yaad rakhne ke liye: "Steal, then Sterilize" — pointer chura lo, source ko khaali kar do. Self-move check (this != &o) bhi rakho, kyunki sort jaise algorithms mein wo case aa jata hai.

Go deeper — visual, from zero

Test yourself — C++ Programming

Connections