Copy and Move Semantics — Rule of Three/Five, Move Constructor, Move Assignment
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:
- Destructor — release the resource
- Copy constructor — create a new object as a copy
- 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
- What are the five special member functions (rule of five)?
- When is a move constructor called instead of a copy constructor?
- Implement the copy-and-swap idiom for a
DynamicArrayclass. - Why should move constructors be marked
noexcept? - 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
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