Skip to content

Copy and Move Semantics — Rule of Three/Five, Move Constructor, Move Assignment

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Copy and Move Semantics. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ copy semantics create object duplicates through copy constructors and copy assignment, while move semantics transfer resources efficiently using move constructors and move assignment introduced in C++11.

What You'll Learn

You will understand the rule of three (C++98) and rule of five (C++11), implement copy and move constructors and assignment operators, distinguish between lvalues and rvalues, use std::move correctly, apply the copy-and-swap idiom for exception safety, and know when to let the compiler generate default implementations.

Why It Matters

Copy and move semantics define how objects in C++ are passed, returned, and stored. The default member-wise copy (shallow copy) is catastrophically wrong for classes that manage resources. Move semantics eliminate redundant copies of temporary objects, which is why std::vector.push_back() can be dozens of times faster with movable types. These concepts are essential for writing efficient, correct C++.

Learning Path

graph LR
    A["19: Operator Overloading"] --> B["20: Copy & Move Semantics"]
    B --> C["21: Pointers"]
    C --> D["22: References"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

The Rule of Three

If a class manages a resource (heap memory, file handle, mutex), you likely need:

  1. Destructor — release the resource
  2. Copy constructor — create a new object as a copy
  3. Copy assignment operator — assign one object to another
#include <iostream>
#include <cstring>

class String {
private:
    char* data_;
    size_t size_;
    
public:
    String(const char* str = "") : size_(std::strlen(str)), data_(new char[size_ + 1]) {
        std::strcpy(data_, str);
        std::cout << "Construct: " << data_ << "\n";
    }
    
    // Destructor
    ~String() {
        delete[] data_;
        std::cout << "Destroy\n";
    }
    
    // Copy constructor
    String(const String& other) : size_(other.size_), data_(new char[other.size_ + 1]) {
        std::strcpy(data_, other.data_);
        std::cout << "Copy: " << data_ << "\n";
    }
    
    // Copy assignment (rule of three)
    String& operator=(const String& other) {
        std::cout << "Copy assign\n";
        if (this != &other) {
            delete[] data_;
            size_ = other.size_;
            data_ = new char[size_ + 1];
            std::strcpy(data_, other.data_);
        }
        return *this;
    }
    
    void print() const { std::cout << data_ << "\n"; }
};

int main() {
    String s1("Hello");
    String s2 = s1;    // copy constructor
    String s3("World");
    s3 = s1;           // copy assignment
}

The Rule of Five (C++11)

With move semantics, add: 4. Move constructor — transfer resources from a temporary 5. Move assignment operator — transfer resources from a temporary

#include <iostream>
#include <cstring>
#include <utility>

class Buffer {
private:
    int* data_;
    size_t size_;
    
public:
    Buffer(size_t size) : data_(new int[size]()), size_(size) {
        std::cout << "Construct " << size << " elements\n";
    }
    
    ~Buffer() {
        delete[] data_;
        std::cout << "Destroy\n";
    }
    
    // Copy constructor
    Buffer(const Buffer& other) : data_(new int[other.size_]), size_(other.size_) {
        std::copy(other.data_, other.data_ + size_, data_);
        std::cout << "Copy\n";
    }
    
    // Copy assignment
    Buffer& operator=(const Buffer& other) {
        std::cout << "Copy assign\n";
        if (this != &other) {
            delete[] data_;
            size_ = other.size_;
            data_ = new int[size_];
            std::copy(other.data_, other.data_ + size_, data_);
        }
        return *this;
    }
    
    // Move constructor
    Buffer(Buffer&& other) noexcept : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
        std::cout << "Move\n";
    }
    
    // Move assignment
    Buffer& operator=(Buffer&& other) noexcept {
        std::cout << "Move assign\n";
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }
    
    size_t size() const { return size_; }
    int& operator[](size_t i) { return data_[i]; }
};

Buffer createBuffer() {
    Buffer b(100);
    b[0] = 42;
    return b;  // move constructor (or RVO)
}

int main() {
    Buffer b1 = createBuffer();  // move from temporary
    Buffer b2(10);
    b2 = std::move(b1);  // move assignment
}

The Copy-and-Swap Idiom

#include <iostream>
#include <cstring>
#include <utility>

class String {
private:
    char* data_;
    size_t size_;
    
public:
    String(const char* str = "") : size_(std::strlen(str)), data_(new char[size_ + 1]) {
        std::strcpy(data_, str);
    }
    
    ~String() { delete[] data_; }
    
    String(const String& other) : size_(other.size_), data_(new char[other.size_ + 1]) {
        std::strcpy(data_, other.data_);
    }
    
    // Move constructor
    String(String&& other) noexcept : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }
    
    // Copy-and-swap assignment (handles both copy and move)
    friend void swap(String& a, String& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }
    
    String& operator=(String other) noexcept {  // pass by value (copy or move)
        swap(*this, other);  // swap with the temporary
        return *this;         // temporary is destroyed, releases old resource
    }
    
    void print() const { std::cout << data_ << "\n"; }
};

The copy-and-swap idiom provides strong exception safety and unifies copy and move assignment into a single function. It creates a copy (or move) as a parameter, then swaps with it.

Lvalues and Rvalues

#include <iostream>
#include <string>
#include <vector>

void check(const std::string& s) {
    std::cout << "lvalue ref\n";
}

void check(std::string&& s) {
    std::cout << "rvalue ref\n";
}

int main() {
    std::string s = "hello";
    check(s);              // lvalue ref
    check("world");        // rvalue ref
    check(std::move(s));   // rvalue ref
    
    // std::move does not move anything; it casts to rvalue reference
    // The actual move happens in the move constructor/assignment
}
  • lvalue: has a name, persists beyond a single expression (s, x, arr[0])
  • rvalue: temporary, will be destroyed soon (42, "hello", std::move(x))

When the Compiler Generates Special Members

Special Member Implicitly Generated If...
Default constructor No user-declared constructors
Destructor Not declared
Copy constructor No user-declared move constructor/move assignment
Copy assignment No user-declared move constructor/move assignment
Move constructor No user-declared copy/move/destructor
Move assignment No user-declared copy/move/destructor

The rule of zero: if your class does not manage a resource, let the compiler generate all special members.

Common Mistakes

Mistake 1: Self-Assignment Check Missing

String& operator=(const String& other) {
    delete[] data_;  // if this == &other, data_ is already gone!
    ...
}

Always include if (this != &other) or use copy-and-swap.

Mistake 2: Move Constructor Not noexcept

Buffer(Buffer&& other) { ... }  // missing noexcept

Standard containers prefer noexcept moves. Without it, std::vector will copy instead of move during reallocation.

Mistake 3: Leaving Moved-From Object in Unspecified State

After a move, the source must be in a valid but unspecified state. Typically, set pointers to nullptr and sizes to 0.

Mistake 4: Using std::move on const Objects

const Buffer cb;
Buffer b = std::move(cb);  // calls copy constructor, not move

std::move on const produces const T&&, which cannot bind to T&& move constructors.

Mistake 5: Forgetting Virtual Destructor

If a class is intended as a base, make the destructor virtual even if you follow the rule of five.

Mistake 6: Over-Implementing When Not Needed

If your class members are all RAII types (vector, string, unique_ptr), let the compiler generate all special members. Manually writing them adds bugs.

Practice Questions

  1. What are the five special member functions (rule of five)?
  2. When is a move constructor called instead of a copy constructor?
  3. Implement the copy-and-swap idiom for a DynamicArray class.
  4. Why should move constructors be marked noexcept?
  5. What is the rule of zero? Give an example class that follows it.

Challenge

Write a SharedMemory class that uses move semantics to transfer ownership of a memory-mapped region. Implement the rule of five. Then demonstrate how moving is cheaper than copying.

FAQ

What is the difference between `std::move` and a move constructor?

std::move is just a cast to rvalue reference. It does not move anything. The move constructor is what actually transfers resources.

What is a 'trivially copyable' type?

A type that can be copied by just copying its bytes (memcpy). It has no user-defined copy/move operations or virtual functions.

Can a move constructor throw?

It can, but it should not. Standard containers optimize based on noexcept moves. A throwing move constructor forces containers to copy instead.

What is copy elision?

The compiler can omit copy/move operations in certain contexts: return value optimization (RVO) and named return value optimization (NRVO). C++17 guarantees copy elision in some cases.

How does `std::vector` use move semantics?

When reallocating, vector moves elements to new storage if their move constructor is noexcept. Otherwise, it copies them. This is why noexcept matters.

What is the `swap` function used in copy-and-swap?

std::swap exchanges the contents of two objects. A custom swap for your class should be noexcept and efficiently swaps pointers/integers.

Mini Project

Build a DynamicArray with full move support:

#include <iostream>
#include <utility>
#include <algorithm>

class DynamicArray {
private:
    int* data_;
    size_t size_;
    
public:
    DynamicArray(size_t size = 0) : data_(size ? new int[size]() : nullptr), size_(size) {
        std::cout << "Construct\n";
    }
    
    ~DynamicArray() {
        delete[] data_;
        std::cout << "Destroy\n";
    }
    
    DynamicArray(const DynamicArray& other)
        : data_(other.size_ ? new int[other.size_] : nullptr), size_(other.size_) {
        std::copy(other.data_, other.data_ + size_, data_);
        std::cout << "Copy\n";
    }
    
    DynamicArray(DynamicArray&& other) noexcept
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
        std::cout << "Move\n";
    }
    
    DynamicArray& operator=(DynamicArray other) noexcept {
        swap(*this, other);
        std::cout << "Assign (copy-and-swap)\n";
        return *this;
    }
    
    friend void swap(DynamicArray& a, DynamicArray& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }
    
    int& operator[](size_t i) { return data_[i]; }
    size_t size() const { return size_; }
};

DynamicArray makeArray(size_t n) {
    DynamicArray arr(n);
    for (size_t i = 0; i < n; ++i) arr[i] = static_cast<int>(i);
    return arr;
}

int main() {
    DynamicArray a = makeArray(10);
    DynamicArray b(5);
    b = a;
    DynamicArray c = std::move(a);
    std::cout << "c[3] = " << c[3] << "\n";
}

What's Next

Copy and move semantics govern how objects pass through functions. The next lesson begins Module 3 on Memory and Pointers: you will learn about pointer declaration, dereferencing, nullptr, and void*.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro