Move semantics, introduced in C++11, is a feature that allows the resources owned by a temporary or expiring object to be transferred (moved) to another object instead of being duplicated through an expensive copy. By implementing a move constructor and move assignment operator, a class can “steal” heap-allocated memory, file handles, or other resources from a source object — leaving the source in a valid but empty state — making operations on large objects nearly instantaneous regardless of their data size.
Introduction
Before C++11, every time you returned a large object from a function, stored it in a container, or passed it by value, the compiler had no choice but to copy all of its data. For small objects, this is negligible. For objects managing large buffers — a vector of a million integers, a string holding a megabyte of text, a matrix of floating-point values — every copy meant allocating new heap memory and duplicating every byte. Programs that moved large amounts of data around were plagued by unnecessary allocations and memory bandwidth consumption.
C++11 solved this problem elegantly with move semantics. The key insight is that when the source of an assignment or initialization is a temporary — an object that is about to be destroyed anyway — there is no reason to copy its data. You can simply transfer ownership of the data by swapping a few pointers. The result is code that is just as safe and correct as copy-based code, but often orders of magnitude faster.
Move semantics are not just a performance trick. They also enable types that logically should not be copyable — like std::unique_ptr, std::thread, and std::fstream — to still be returned from functions and stored in containers. These types represent exclusive ownership of a resource, so copying them makes no semantic sense, but moving them transfers that ownership cleanly.
This article builds a complete understanding of move semantics from the ground up. You will understand the problem, learn to write correct move constructors and move assignment operators, see how std::move enables explicit moves from named objects, measure real performance differences, and internalize the rules that govern when the compiler automatically selects move operations. Every concept is demonstrated with runnable, thoroughly explained code.
The Cost of Copying: Seeing the Problem
The best way to appreciate move semantics is to first observe the cost it eliminates. Let’s build a class that manages a large heap buffer and instrument every operation.
#include <iostream>
#include <cstring>
#include <chrono>
using namespace std;
class Buffer {
public:
size_t size;
int* data;
// Constructor: allocate heap buffer
explicit Buffer(size_t n) : size(n), data(new int[n]) {
fill(data, data + n, 0);
cout << "[Buffer] Constructed, size=" << n << endl;
}
// Copy constructor: must duplicate entire heap buffer
Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
memcpy(data, other.data, size * sizeof(int));
cout << "[Buffer] COPIED, size=" << size << " (expensive!)" << endl;
}
// Copy assignment operator
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data;
size = other.size;
data = new int[size];
memcpy(data, other.data, size * sizeof(int));
cout << "[Buffer] COPY ASSIGNED, size=" << size << endl;
}
return *this;
}
// Destructor
~Buffer() {
delete[] data;
cout << "[Buffer] Destroyed, size=" << size << endl;
}
};
Buffer makeBuffer(size_t n) {
Buffer b(n);
// Fill with some values
for (size_t i = 0; i < n; i++) b.data[i] = static_cast<int>(i);
return b; // Without move semantics: copied out of function
}
int main() {
cout << "=== Creating buffer via factory ===" << endl;
Buffer b = makeBuffer(5);
cout << "\n=== Copying to another buffer ===" << endl;
Buffer b2 = b; // Must copy — b still needed
cout << "\nFirst 3 values of b2: "
<< b2.data[0] << " " << b2.data[1] << " " << b2.data[2] << endl;
return 0;
}Output (without move semantics, simplified):
=== Creating buffer via factory ===
[Buffer] Constructed, size=5
[Buffer] COPIED, size=5 (expensive!)
[Buffer] Destroyed, size=5
=== Copying to another buffer ===
[Buffer] COPIED, size=5 (expensive!)
First 3 values of b2: 0 1 2
[Buffer] Destroyed, size=5
[Buffer] Destroyed, size=5Step-by-step explanation:
makeBuffer(5)constructs aBufferlocally. Without optimization or move semantics, returning it requires copying the entire heap buffer into the caller’sbvariable, then destroying the original.Buffer b2 = bcopiesbintob2. This is correct —bis still needed afterwards. But copying means allocating new memory and copying every integer.- For a buffer of 5 elements this is trivial. For a buffer of 10 million elements, each copy means allocating 40 MB of memory and copying 40 MB of data. A program that processes large buffers frequently will spend most of its time in
memcpyand heap allocation, not in actual work.
This is the problem move semantics is designed to eliminate.
Writing a Move Constructor
A move constructor takes an rvalue reference to the source object (T&&) and transfers ownership of its resources, leaving the source in a valid but empty state.
#include <iostream>
#include <cstring>
using namespace std;
class Buffer {
public:
size_t size;
int* data;
// Regular constructor
explicit Buffer(size_t n) : size(n), data(new int[n]) {
fill(data, data + n, 0);
cout << "[Buffer] Constructed, size=" << n << endl;
}
// Copy constructor: duplicate everything
Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
memcpy(data, other.data, size * sizeof(int));
cout << "[Buffer] COPIED, size=" << size << endl;
}
// *** MOVE CONSTRUCTOR: transfer ownership, don't copy ***
Buffer(Buffer&& other) noexcept
: size(other.size), data(other.data) // Take other's pointer
{
other.size = 0; // Leave source in valid empty state
other.data = nullptr; // Prevent double-free in source's destructor
cout << "[Buffer] MOVED, size=" << size << " (cheap!)" << endl;
}
// Destructor
~Buffer() {
delete[] data; // delete nullptr is a no-op — safe
if (size > 0)
cout << "[Buffer] Destroyed, size=" << size << endl;
else
cout << "[Buffer] Destroyed (was moved-from)" << endl;
}
};
Buffer makeBuffer(size_t n) {
Buffer b(n);
for (size_t i = 0; i < n; i++) b.data[i] = static_cast<int>(i * 10);
return b; // Compiler uses move constructor (or RVO)
}
int main() {
cout << "=== Creating via factory ===" << endl;
Buffer b1 = makeBuffer(5);
cout << "b1.data[2] = " << b1.data[2] << endl;
cout << "\n=== Moving explicitly ===" << endl;
Buffer b2 = move(b1); // Explicitly move b1 into b2
cout << "b2.data[2] = " << b2.data[2] << endl;
cout << "b1.data is " << (b1.data ? "non-null" : "null")
<< ", b1.size = " << b1.size << endl;
return 0;
}Output:
=== Creating via factory ===
[Buffer] Constructed, size=5
[Buffer] MOVED, size=5 (cheap!)
[Buffer] Destroyed (was moved-from)
=== Moving explicitly ===
[Buffer] MOVED, size=5 (cheap!)
b2.data[2] = 20
b1.data is null, b1.size = 0
[Buffer] Destroyed (was moved-from)
[Buffer] Destroyed, size=5Step-by-step explanation:
- The move constructor
Buffer(Buffer&& other) noexcepttakes an rvalue reference. It initializessizeanddatafromother— this simply copies two values (an integer and a pointer), which is essentially free regardless of how large the buffer is. - Critically, it then sets
other.size = 0andother.data = nullptr. This is called nullifying the source. It leavesotherin a valid state that can be safely destroyed — and whenother‘s destructor runs,delete[] nullptris a no-op that does nothing. - Without nullifying the source, both
b1andb2would hold the samedatapointer. When both destructors run,delete[] datawould be called twice on the same memory — a double-free bug causing undefined behavior. - The move constructor is marked
noexcept. This is important: standard containers likestd::vectorwill only use move operations during reallocation if they are guaranteed not to throw. Withoutnoexcept, the container falls back to copying for safety. Buffer b2 = move(b1)explicitly convertsb1(an lvalue) to an rvalue, triggering the move constructor. After this,b1is in the valid-but-empty state:dataisnullptrandsizeis0. You should not useb1‘s data after this — only reassign it or let it be destroyed.
Writing a Move Assignment Operator
The move assignment operator handles the case where you assign from an rvalue to an already-existing object. It must release the existing resources before taking ownership of the new ones.
#include <iostream>
#include <cstring>
using namespace std;
class Buffer {
public:
size_t size;
int* data;
explicit Buffer(size_t n) : size(n), data(new int[n]) {
fill(data, data + n, 0);
}
// Copy constructor
Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
memcpy(data, other.data, size * sizeof(int));
cout << "[=] Copy constructor" << endl;
}
// Move constructor
Buffer(Buffer&& other) noexcept : size(other.size), data(other.data) {
other.size = 0;
other.data = nullptr;
cout << "[=] Move constructor" << endl;
}
// Copy assignment operator
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data; // Release old resource
size = other.size;
data = new int[size]; // Allocate new resource
memcpy(data, other.data, size * sizeof(int));
cout << "[=] Copy assignment" << endl;
}
return *this;
}
// *** MOVE ASSIGNMENT OPERATOR ***
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data; // Release THIS object's old resource
// Take ownership of other's resource
size = other.size;
data = other.data;
// Nullify the source
other.size = 0;
other.data = nullptr;
cout << "[=] Move assignment" << endl;
}
return *this;
}
~Buffer() { delete[] data; }
};
Buffer makeTemp(size_t n) {
return Buffer(n);
}
int main() {
Buffer b1(10);
Buffer b2(5);
cout << "Before assignments: b1.size=" << b1.size
<< ", b2.size=" << b2.size << endl;
// Move assignment: b2 receives temporary's resources, b2's old data freed
b2 = makeTemp(20);
cout << "After move assign: b2.size=" << b2.size << endl;
// Copy assignment: b1 is still needed after this
Buffer b3(3);
b3 = b1;
cout << "After copy assign: b3.size=" << b3.size << endl;
// Explicit move assignment
b1 = move(b2);
cout << "After explicit move: b1.size=" << b1.size
<< ", b2.size=" << b2.size << endl;
return 0;
}Output:
Before assignments: b1.size=10, b2.size=5
[=] Move assignment
After move assign: b2.size=20
[=] Copy assignment
After copy assign: b3.size=10
[=] Move assignment
After explicit move: b1.size=20, b2.size=0Step-by-step explanation:
b2 = makeTemp(20)—makeTemp(20)returns a temporaryBuffer(an rvalue). The move assignment operator is selected. It first callsdelete[] datato freeb2‘s existing 5-element array, then takes ownership of the temporary’s 20-element array by copying its pointer. The temporary is then nullified. Net cost: onedeleteand zero allocations.b3 = b1—b1is an lvalue. The copy assignment operator is selected. It allocates a new array and copies all ofb1‘s data.b1is unchanged.b1 = move(b2)—std::move(b2)produces an rvalue. The move assignment operator fires again: it freesb1‘s old 10-element array, takes ownership ofb2‘s 20-element array, and nullifiesb2.b1now has size 20,b2has size 0.- The self-assignment check
if (this != &other)is a safety guard. Whileb1 = move(b1)is a logical mistake (moving from yourself), the check prevents catastrophic behavior: without it,delete[] datawould free the memory, and then copyingdatainto itself would access freed memory.
The Rule of Five
In C++11 and later, if a class needs a custom destructor, it almost certainly needs a custom implementation (or explicit = delete or = default) for all five special member functions. This is the Rule of Five — the move-semantics-aware evolution of the older Rule of Three.
class Resource {
public:
Resource(size_t n); // Regular constructor
~Resource(); // 1. Destructor
Resource(const Resource&); // 2. Copy constructor
Resource& operator=(const Resource&); // 3. Copy assignment
Resource(Resource&&) noexcept; // 4. Move constructor (C++11)
Resource& operator=(Resource&&) noexcept; // 5. Move assignment (C++11)
};The reasoning is straightforward: if you need a custom destructor, your class is managing a resource. If it manages a resource, copying it requires careful deep-copy logic (rules 2 and 3), and moving it enables efficient resource transfer (rules 4 and 5).
If you define any one of these five, you should explicitly define or = delete all five to make your intent clear.
class MoveOnly {
int* data;
size_t size;
public:
explicit MoveOnly(size_t n) : size(n), data(new int[n]) {}
~MoveOnly() { delete[] data; }
// Disallow copying explicitly
MoveOnly(const MoveOnly&) = delete;
MoveOnly& operator=(const MoveOnly&) = delete;
// Allow moving
MoveOnly(MoveOnly&& other) noexcept
: size(other.size), data(other.data) {
other.size = 0; other.data = nullptr;
}
MoveOnly& operator=(MoveOnly&& other) noexcept {
if (this != &other) {
delete[] data;
size = other.size; data = other.data;
other.size = 0; other.data = nullptr;
}
return *this;
}
};MoveOnly is a type like std::unique_ptr — it can be moved into containers and returned from functions, but it cannot be accidentally copied. The = delete declarations make any attempt to copy a compile-time error with a clear message.
Measuring the Real Performance Impact
Move semantics are not just a theoretical improvement — they produce measurable, dramatic performance gains for classes that manage large resources. Here is a benchmark demonstrating the difference.
#include <iostream>
#include <vector>
#include <chrono>
#include <numeric>
using namespace std;
// Simulates an object holding a large buffer
struct LargeData {
static const size_t SIZE = 1'000'000;
vector<double> values;
LargeData() : values(SIZE, 1.0) {}
// Explicit copy for benchmarking
LargeData deepCopy() const {
return LargeData(*this); // Force copy
}
};
template<typename Func>
double measureMs(Func f, int iterations = 100) {
auto start = chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; i++) f();
auto end = chrono::high_resolution_clock::now();
return chrono::duration<double, milli>(end - start).count() / iterations;
}
int main() {
LargeData source;
// Benchmark: copy construction
double copyTime = measureMs([&]() {
LargeData copied = source; // Copy: allocates + copies 1M doubles
(void)copied;
});
// Benchmark: move construction
double moveTime = measureMs([&]() {
LargeData temp = source; // Copy to temp
LargeData moved = move(temp); // Move: just pointer swap
(void)moved;
});
cout << "Average copy construction: " << copyTime << " ms" << endl;
cout << "Average move construction: " << moveTime << " ms" << endl;
cout << "Speedup factor: ~" << (int)(copyTime / moveTime) << "x" << endl;
// Benchmark: vector of large objects
auto fillByCopy = [&]() {
vector<LargeData> v;
v.reserve(5);
for (int i = 0; i < 5; i++) v.push_back(source); // Copies
};
auto fillByMove = [&]() {
vector<LargeData> v;
v.reserve(5);
for (int i = 0; i < 5; i++) {
LargeData temp = source;
v.push_back(move(temp)); // Moves
}
};
double vecCopyTime = measureMs(fillByCopy, 20);
double vecMoveTime = measureMs(fillByMove, 20);
cout << "\nVector fill (5 elements) by copy: " << vecCopyTime << " ms" << endl;
cout << "Vector fill (5 elements) by move: " << vecMoveTime << " ms" << endl;
return 0;
}Typical output (actual times vary by hardware):
Average copy construction: 2.84 ms
Average move construction: 0.003 ms
Speedup factor: ~947x
Vector fill (5 elements) by copy: 14.2 ms
Vector fill (5 elements) by move: 0.015 msStep-by-step explanation:
- Copying a
LargeDataobject requires allocating 8 MB of memory (1,000,000doublevalues × 8 bytes) and copying all 8 MB. This takes several milliseconds even on modern hardware. - Moving a
LargeDataobject transfers the internalvector<double>by swapping three values: a pointer, a size, and a capacity. This is three machine-word writes — effectively zero time at this scale. - The speedup factor of ~1000x is representative of real-world scenarios involving large data objects. For programs that frequently pass, return, or store such objects, move semantics is not a micro-optimization — it is a fundamental enabler of practical performance.
- The vector benchmark shows the compounding benefit: filling a vector with 5 large objects takes 5 copies (5 × ~2.84 ms = ~14 ms) compared to 5 moves (5 × ~0.003 ms ≈ negligible).
When the Compiler Automatically Uses Move Semantics
You do not always need to write std::move explicitly. The compiler automatically selects move operations in several important situations.
Returning Local Objects from Functions
When a function returns a local variable by value, the compiler can apply Return Value Optimization (RVO) or Named Return Value Optimization (NRVO) to construct the object directly in the caller’s storage — eliminating both copies and moves entirely. If RVO is not applicable, the compiler will still automatically move (not copy) the returned object.
#include <iostream>
#include <vector>
using namespace std;
vector<int> buildVector(int n) {
vector<int> result;
result.reserve(n);
for (int i = 0; i < n; i++) result.push_back(i);
return result; // Compiler applies RVO or move — never copies
}
int main() {
// No explicit std::move needed — compiler handles it
vector<int> v = buildVector(1000000);
cout << "Vector size: " << v.size() << endl;
return 0;
}Important rule: Never write return std::move(result) for a local variable. Doing so prevents RVO (because the expression becomes a named rvalue reference, which is not a prvalue) and forces a move where RVO would have been free. Simply write return result and let the compiler choose the best strategy.
Temporaries Passed to Functions
When you pass a temporary (rvalue) to a function that accepts T or T&&, the compiler selects the move constructor automatically.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int main() {
vector<string> words;
// Temporary string: automatically moved into the vector
words.push_back(string("hello")); // move selected — string is a temporary
// String literal: temporary created and moved
words.push_back("world"); // Implicit conversion to string, then moved
// Named variable: copied (because it's an lvalue)
string greeting = "hi there";
words.push_back(greeting); // Copied — greeting still needed
// Named variable after explicit move: moved
words.push_back(move(greeting)); // greeting is now empty
for (const auto& w : words) cout << w << " ";
cout << endl;
return 0;
}Output:
hello world hi there hi there Moving Into Containers During Reallocation
When a std::vector grows and needs to reallocate its internal buffer, it moves (not copies) its elements into the new buffer — but only if the element type’s move constructor is marked noexcept. If it is not noexcept, the vector must use copying to maintain the strong exception guarantee (either all elements are moved, or none are).
#include <iostream>
#include <vector>
using namespace std;
struct SafeToMove {
int id;
SafeToMove(int i) : id(i) {}
SafeToMove(SafeToMove&& other) noexcept : id(other.id) {
cout << "Moving element " << id << endl;
}
SafeToMove(const SafeToMove& other) : id(other.id) {
cout << "Copying element " << id << endl;
}
};
int main() {
vector<SafeToMove> v;
v.reserve(2);
v.emplace_back(1);
v.emplace_back(2);
cout << "--- Vector reallocation ---" << endl;
v.emplace_back(3); // Exceeds capacity, triggers reallocation
// Elements 1 and 2 are MOVED into new buffer (noexcept move constructor)
return 0;
}Output:
--- Vector reallocation ---
Moving element 1
Moving element 2Elements 1 and 2 are moved, not copied, into the larger buffer because SafeToMove‘s move constructor is noexcept. Change noexcept to nothing and rerun — you will see “Copying element” instead.
Move Semantics with Standard Library Containers
All standard library containers in C++11 and later provide move constructors and move assignment operators that run in O(1) time — they transfer ownership of the internal heap buffer rather than copying its elements.
#include <iostream>
#include <vector>
#include <string>
#include <map>
using namespace std;
int main() {
// --- Moving a vector ---
vector<int> big(10'000'000, 42);
cout << "big.size() = " << big.size() << endl;
auto start = chrono::high_resolution_clock::now();
vector<int> moved = move(big); // O(1): transfers internal pointer
auto end = chrono::high_resolution_clock::now();
cout << "moved.size() = " << moved.size() << endl;
cout << "big.size() after move = " << big.size() << endl; // 0
// --- Moving a string ---
string longText(1'000'000, 'x');
string movedText = move(longText);
cout << "movedText.size() = " << movedText.size() << endl;
cout << "longText.size() after move = " << longText.size() << endl; // 0
// --- Moving a map ---
map<string, int> scores;
for (int i = 0; i < 100000; i++) {
scores["key" + to_string(i)] = i;
}
cout << "scores.size() = " << scores.size() << endl;
map<string, int> movedScores = move(scores); // O(1)
cout << "movedScores.size() = " << movedScores.size() << endl;
cout << "scores.size() after move = " << scores.size() << endl; // 0
return 0;
}Output:
big.size() = 10000000
moved.size() = 10000000
big.size() after move = 0
movedText.size() = 1000000
longText.size() after move = 0
scores.size() = 100000
movedScores.size() = 100000
scores.size() after move = 0Step-by-step explanation:
- Moving a
vector<int>with 10 million elements takes essentially zero time. The move constructor copies three values (pointer to data, size, capacity) from the source vector and sets the source’s size to zero. - Moving a
stringof 1 million characters is equally instant — the internal character buffer is transferred by pointer, not by copying characters. - Moving a
mapwith 100,000 entries is O(1). Astd::mapis a red-black tree; moving it transfers the root pointer and element count — no tree traversal or node allocation occurs. - All of these moved-from containers are left in valid empty states:
size() == 0, fully usable for new insertions.
Move Semantics in Class Design: A Complete Example
Let’s put everything together with a production-quality DynamicArray class that implements all five special member functions correctly.
#include <iostream>
#include <algorithm>
#include <stdexcept>
using namespace std;
template<typename T>
class DynamicArray {
public:
// Constructor
explicit DynamicArray(size_t capacity = 0)
: data_(capacity ? new T[capacity] : nullptr),
size_(0),
capacity_(capacity) {
cout << "DynamicArray constructed, capacity=" << capacity << endl;
}
// Destructor
~DynamicArray() {
delete[] data_;
cout << "DynamicArray destroyed, size=" << size_ << endl;
}
// Copy constructor: deep copy
DynamicArray(const DynamicArray& other)
: data_(other.capacity_ ? new T[other.capacity_] : nullptr),
size_(other.size_),
capacity_(other.capacity_) {
copy(other.data_, other.data_ + other.size_, data_);
cout << "DynamicArray COPY constructed, size=" << size_ << endl;
}
// Move constructor: steal resources
DynamicArray(DynamicArray&& other) noexcept
: data_(other.data_),
size_(other.size_),
capacity_(other.capacity_) {
other.data_ = nullptr;
other.size_ = 0;
other.capacity_ = 0;
cout << "DynamicArray MOVE constructed, size=" << size_ << endl;
}
// Copy assignment: deep copy with self-assignment guard
DynamicArray& operator=(const DynamicArray& other) {
if (this != &other) {
DynamicArray temp(other); // Copy-and-swap idiom
swap(temp);
}
cout << "DynamicArray COPY assigned, size=" << size_ << endl;
return *this;
}
// Move assignment: transfer resources
DynamicArray& operator=(DynamicArray&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
capacity_ = other.capacity_;
other.data_ = nullptr;
other.size_ = 0;
other.capacity_ = 0;
}
cout << "DynamicArray MOVE assigned, size=" << size_ << endl;
return *this;
}
// Add element
void push_back(const T& value) {
if (size_ == capacity_) grow();
data_[size_++] = value;
}
void push_back(T&& value) {
if (size_ == capacity_) grow();
data_[size_++] = move(value);
}
// Element access
T& operator[](size_t i) {
if (i >= size_) throw out_of_range("Index out of range");
return data_[i];
}
size_t size() const { return size_; }
size_t capacity() const { return capacity_; }
void swap(DynamicArray& other) noexcept {
std::swap(data_, other.data_);
std::swap(size_, other.size_);
std::swap(capacity_, other.capacity_);
}
private:
void grow() {
size_t newCap = capacity_ ? capacity_ * 2 : 1;
T* newData = new T[newCap];
move(data_, data_ + size_, newData); // Move elements to new buffer
delete[] data_;
data_ = newData;
capacity_ = newCap;
}
T* data_;
size_t size_;
size_t capacity_;
};
DynamicArray<int> buildArray(int n) {
DynamicArray<int> arr(n);
for (int i = 0; i < n; i++) arr.push_back(i * 2);
return arr; // Moved or RVO'd out — never copied
}
int main() {
cout << "=== Building array ===" << endl;
auto arr = buildArray(5);
cout << "arr[3] = " << arr[3] << endl;
cout << "\n=== Copying array ===" << endl;
DynamicArray<int> arr2 = arr; // Deep copy
arr2.push_back(999);
cout << "\n=== Moving array ===" << endl;
DynamicArray<int> arr3 = move(arr); // arr gives up its resources
cout << "arr3.size() = " << arr3.size() << endl;
cout << "arr.size() after move = " << arr.size() << endl;
cout << "\n=== Move assignment ===" << endl;
arr = move(arr2); // arr (now empty) gets arr2's data
cout << "arr.size() = " << arr.size() << endl;
cout << "\n=== Cleanup ===" << endl;
return 0;
}Output:
=== Building array ===
DynamicArray constructed, capacity=5
DynamicArray MOVE constructed, size=5
DynamicArray destroyed, size=0
arr[3] = 6
=== Copying array ===
DynamicArray COPY constructed, size=5
DynamicArray COPY assigned, size=5
=== Moving array ===
DynamicArray MOVE constructed, size=5
arr3.size() = 5
arr.size() after move = 0
=== Move assignment ===
DynamicArray MOVE assigned, size=6
arr.size() = 6
=== Cleanup ===
DynamicArray destroyed, size=0
DynamicArray destroyed, size=6
DynamicArray destroyed, size=5Step-by-step explanation:
buildArray(5)constructs aDynamicArraylocally and returns it. The compiler applies RVO or the move constructor to transfer it toarrwithout copying. The local variable inside the function is destroyed afterward (size=0, already moved).DynamicArray<int> arr2 = arrtriggers the copy constructor.arr2gets a deep copy of all elements. Later,arr2.push_back(999)adds an element toarr2without affectingarr.DynamicArray<int> arr3 = move(arr)uses the move constructor.arr‘s internal buffer is transferred toarr3in O(1) — no allocation, no element copying.arris left empty.arr = move(arr2)uses the move assignment operator.arr‘s current resources (empty buffer) are freed, and it takes ownership ofarr2‘s buffer (6 elements including the 999).- The
grow()method usesstd::moveto move elements when resizing — even inside the array implementation, move semantics are used for efficiency.
Move Semantics for Non-Copyable Types
Move semantics enables types that cannot be copied to still be useful in containers and function return values. std::unique_ptr is the canonical example, but you can design your own exclusive-ownership types.
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
class NetworkConnection {
int socketFd_;
string host_;
public:
NetworkConnection(string host, int fd)
: socketFd_(fd), host_(move(host)) {
cout << "Connected to " << host_ << " (fd=" << socketFd_ << ")" << endl;
}
~NetworkConnection() {
if (socketFd_ != -1) {
cout << "Closing connection to " << host_
<< " (fd=" << socketFd_ << ")" << endl;
}
}
// Non-copyable: a socket cannot be duplicated
NetworkConnection(const NetworkConnection&) = delete;
NetworkConnection& operator=(const NetworkConnection&) = delete;
// Movable: ownership of the socket can be transferred
NetworkConnection(NetworkConnection&& other) noexcept
: socketFd_(other.socketFd_), host_(move(other.host_)) {
other.socketFd_ = -1; // Prevent other from closing the socket
cout << "Connection moved" << endl;
}
NetworkConnection& operator=(NetworkConnection&& other) noexcept {
if (this != &other) {
if (socketFd_ != -1) {
cout << "Closing old connection (fd=" << socketFd_ << ")" << endl;
}
socketFd_ = other.socketFd_;
host_ = move(other.host_);
other.socketFd_ = -1;
}
return *this;
}
void send(const string& msg) {
if (socketFd_ != -1)
cout << "[fd=" << socketFd_ << "] Sending: " << msg << endl;
}
};
NetworkConnection openConnection(const string& host) {
return NetworkConnection(host, 42); // Returned by move/RVO
}
int main() {
// Returned from function — moved, not copied
NetworkConnection conn = openConnection("api.example.com");
conn.send("GET /data HTTP/1.1");
// Stored in a vector of move-only types
vector<NetworkConnection> pool;
pool.push_back(move(conn)); // Move into vector
pool.push_back(openConnection("db.example.com"));
cout << "Pool size: " << pool.size() << endl;
pool[0].send("pooled request 1");
pool[1].send("pooled request 2");
return 0;
}Output:
Connected to api.example.com (fd=42)
[fd=42] Sending: GET /data HTTP/1.1
Connection moved
Connected to db.example.com (fd=42)
Pool size: 2
[fd=42] Sending: pooled request 1
[fd=42] Sending: pooled request 2
Closing connection to api.example.com (fd=42)
Closing connection to db.example.com (fd=42)Step-by-step explanation:
NetworkConnectiondeletes its copy constructor and copy assignment operator — a socket file descriptor represents a unique OS resource that cannot be meaningfully duplicated.- It implements move operations that transfer ownership of the socket. The moved-from object sets
socketFd_ = -1so its destructor does not close the socket that has been transferred. openConnectionreturns aNetworkConnectionby value. Because it is move-only, the compiler uses the move constructor (or RVO) to transfer it toconn. Without move semantics, this would be a compile error.pool.push_back(move(conn))movesconninto the vector. After this,connis in the moved-from state (socketFd_ = -1) and should not be used.- The vector stores the connections and properly closes them via destructor when it goes out of scope. This is RAII + move semantics working together: exclusive ownership, automatic cleanup, and efficient transfer.
Common Move Semantics Mistakes and How to Avoid Them
Mistake 1: Using an Object After Moving From It
string s = "hello";
string t = move(s);
cout << s; // Compiles, but s is in unspecified state
cout << s.size(); // Typically 0 for string, but unspecified
s = "new value"; // Safe: reassigning a moved-from object is always OKAfter moving, reassign before reuse. Never read from a moved-from object unless you know the specific guarantee the type provides (e.g., std::string guarantees the moved-from state is valid and typically empty).
Mistake 2: Forgetting noexcept on Move Operations
// BAD: vector won't use this move constructor during reallocation
MyClass(MyClass&& other) { ... }
// GOOD: vector will move instead of copy during reallocation
MyClass(MyClass&& other) noexcept { ... }Mistake 3: Applying std::move to a Return Statement
// BAD: prevents RVO, forces a move where zero-cost was possible
string buildString() {
string s = "hello";
return move(s); // Disables NRVO — never do this
}
// GOOD: compiler applies NRVO automatically
string buildString() {
string s = "hello";
return s; // Compiler moves or applies RVO
}Mistake 4: Moving from const Objects
const string s = "data";
string t = move(s); // Silently copies! const T&& doesn't bind to move constructorsstd::move on a const object produces const T&&. Since move constructors take T&& (non-const), the copy constructor is selected instead. The move appears to succeed but copies silently.
Mistake 5: Not Nullifying Pointer Members in Move Constructor
// BAD: double-free when both objects are destroyed
MyClass(MyClass&& other) noexcept : data_(other.data_) {
// Forgot: other.data_ = nullptr;
}
// GOOD: nullify source to prevent double-free
MyClass(MyClass&& other) noexcept : data_(other.data_) {
other.data_ = nullptr;
}Move Semantics Summary: When Each Operation Fires
| Situation | Operation Selected | Reason |
|---|---|---|
T obj = other_obj; (lvalue) | Copy constructor | Source is a named lvalue — must preserve it |
T obj = func(); (returns T) | Move constructor or RVO | Return value is a temporary (rvalue) |
T obj = move(lvalue); | Move constructor | move casts lvalue to rvalue |
obj = other_obj; (lvalue) | Copy assignment | Source is an lvalue |
obj = func(); (returns T) | Move assignment | Return value is temporary |
obj = move(lvalue); | Move assignment | move casts lvalue to rvalue |
vector.push_back(temp) | Move constructor | Temporary argument |
vector.push_back(lval) | Copy constructor | Named lvalue argument |
vector reallocation | Move constructor (if noexcept) | Elements moved to new buffer |
Conclusion
Move semantics is one of the most impactful features introduced in C++11 and is central to modern C++ performance. By recognizing that temporary objects are about to be destroyed and that their resources can be stolen rather than copied, move semantics eliminates an entire class of unnecessary heap allocations and memory bandwidth consumption.
The mechanics are clear once you internalize them: the move constructor and move assignment operator take an rvalue reference (T&&), transfer ownership of the resource by copying pointers (not data), and nullify the source to prevent double-free. std::move is the explicit cast that grants move permission to named lvalues. noexcept is the flag that tells containers and the standard library it is safe to move your type.
Move semantics also fundamentally changed what C++ types can be. Move-only types like unique_ptr, fstream, and thread — types that cannot be copied because they represent exclusive ownership — can now participate fully in the language: they can be returned from functions, stored in standard containers, and passed through layers of code with zero overhead.
Writing the five special member functions correctly — destructor, copy constructor, copy assignment, move constructor, and move assignment — is the foundation of any resource-managing class in modern C++. Master these, and you will write code that is both maximally safe and maximally fast.








