C++ Move Semantics: Copy vs Move Explained
이 글의 핵심
Hands-on guide to C++ move semantics: when copies happen, when moves happen, and how to avoid common mistakes.
Introduction
Move semantics are a feature introduced in C++11 that allows moving the resources of an object without copying them. Objects with large copy costs (vectors, strings, etc.) can be passed efficiently.
Why do you need it?:
- Performance: Improve performance by moving instead of copying
- Ownership Transfer: Explicitly transfer resource ownership
- Uncopyable type: Passing a non-copyable type such as
unique_ptr - Temporary object optimization: Resource reuse of temporary objects
// ❌ Copy: slow (deep copy)
std::vector<int> v1(1000000);// Allocate 1 million elements
std::vector<int> v2 = v1;// Copy all 1 million elements
// movement:
// 1. Allocate new memory for v2 (1 million * sizeof(int))
// 2. Copy all elements from v1 to v2
// 3. Both v1 and v2 have independent memory
// Cost: O(n) time, O(n) memory
// ✅ Move: Fast (shallow copy)
std::vector<int> v1(1000000);// Allocate 1 million elements
std::vector<int> v2 = std::move(v1);// Copy only the pointer (move the internal buffer)
// movement:
// 1. Copy the internal pointer of v1 to v2 (3 pointers: data, size, capacity)
// 2. Set the pointer of v1 to nullptr (invalidate)
// 3. v2 owns v1's memory
// Cost: O(1) time, no additional memory
1. Lvalue vs Rvalue
Basic concepts
int x = 10;// x is an lvalue (has a name, has an address)
// Can be referenced multiple times, memory location confirmed
int y = x + 5;// x+5 is rvalue (temporary value, no address)
// Disappears immediately after expression evaluation, only used once
int* ptr = &x;// ✅ OK: You can get the address of the lvalue
// x is stored in memory
// int* ptr2 = &(x+5);// ❌ Error: Unable to get address of rvalue
// x+5 is temporary register value, no memory address
Key points:
- lvalue: has a name, can be used multiple times, has an address
- rvalue: Temporary value, disappears at the end of the expression, cannot have an address
lvalue and rvalue examples
#include <string>
#include <iostream>
std::string getName() {
return "Alice";
}
int main() {
int x = 10; // x: lvalue
int y = x + 5; // x+5: rvalue
int z = std::move(x); // std::move(x): rvalue
std::string s1 = "hello";
std::string s2 = s1; // s1: lvalue
std::string s3 = s1 + " world"; // s1 + " world": rvalue
std::string name = getName(); // getName(): rvalue
return 0;
}
rvalue reference
int x = 10;
// lvalue reference (T&): Only lvalues can be bound
int& ref1 = x; // ✅ OK: x is lvalue
// int& ref2 = 10;// ❌ Error: 10 is an rvalue (temporary value)
// lvalue references can only be values with memory addresses
// rvalue reference (T&&): Only rvalues can be bound
// int&& ref3 = x;// ❌ Error: x is an lvalue
// rvalue references can only be temporary values
int&& ref4 = 10;// ✅ OK: 10 is rvalue (temporary value)
// rvalue references extend the lifetime of temporary values
// const lvalue reference (const T&): any possible (special rule)
const int& ref5 = x;// ✅ OK: lvalue binding
const int& ref6 = 10;// ✅ OK: rvalue binding (extending temporary value lifetime)
// const reference is the only lvalue reference that can receive a temporary value
2. Copy vs Move
Copy (inefficient)
#include <iostream>
#include <cstring>
class String {
private:
char* data;
size_t size;
public:
String(const char* str) {
size = strlen(str);
data = new char[size + 1];
strcpy(data, str);
std::cout << "constructor" << std::endl;
}
~String() {
delete[] data;
std::cout << "destructor" << std::endl;
}
// copy constructor
String(const String& other) {
size = other.size;
data = new char[size + 1];
strcpy(data, other.data);// deep copy
std::cout << "copy constructor" << std::endl;
}
void print() const {
std::cout << data << std::endl;
}
};
int main() {
String s1("Hello");
String s2 = s1;// Copy occurs (memory allocation + copy)
s2.print();
return 0;
}
output of power:
constructor
copy constructor
Hello
destructor
destructor
Movement (efficient)
#include <iostream>
#include <cstring>
class String {
private:
char* data;
size_t size;
public:
String(const char* str) {
size = strlen(str);
data = new char[size + 1];
strcpy(data, str);
std::cout << "constructor" << std::endl;
}
~String() {
delete[] data;
std::cout << "destructor" << std::endl;
}
// copy constructor
String(const String& other) {
size = other.size;
data = new char[size + 1];
strcpy(data, other.data);
std::cout << "copy constructor" << std::endl;
}
// Move constructor: Receives an rvalue reference (String&&)
// noexcept: ensures no exception is thrown (performance optimization)
String(String&& other) noexcept {
// 1. Copy only the pointer (shallow copy)
data = other.data;// Get other's memory
size = other.size;
// 2. Invalidate the original (important!)
// so as not to delete when other's destructor is called
other.data = nullptr;// original pointer to nullptr
other.size = 0;
std::cout << "move constructor" << std::endl;
// Result: no memory allocation, only pointer movement (very fast)
}
void print() const {
if (data) std::cout << data << std::endl;
else std::cout << "(empty)" << std::endl;
}
};
int main() {
String s1("Hello");
String s2 = std::move(s1);// Move (copy only the pointer, fast!)
s2.print(); // Hello
s1.print();// (empty) - s1 is no longer available
return 0;
}
output of power:
constructor
move constructor
Hello
(empty)
destructor
destructor
3. std::move
Default Enabled
#include <utility>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v1 = {1, 2, 3, 4, 5};
// Copy: Copy all elements to new memory
std::vector<int> v2 = v1;// Copy all elements of v1
// v1 and v2 have independent memory
std::cout << "v1 size: " << v1.size() << std::endl;// 5 (maintain)
std::cout << "v2 size: " << v2.size() << std::endl;// 5 (new allocation)
// Move: Move only the internal buffer pointer
// std::move(v1): Cast v1 to rvalue
// call move constructor → transfer internal buffer of v1 to v3
std::vector<int> v3 = std::move(v1);// Move v1's internal buffer to v3
// v1 is valid but empty (moved-from state)
std::cout << "v1 size: " << v1.size() << std::endl;// 0 (empty)
std::cout << "v3 size: " << v3.size() << std::endl;// 5 (buffer owned by v1)
// NOTE: v1 is deprecated (destructor call is safe)
return 0;
}
Note: std::move does not actually move, it only casts to an rvalue!
std::move implementation
// Conceptual implementation of std::move
// Actually just a simple cast (no movement!)
template<typename T>
typename std::remove_reference<T>::type&& move(T&& arg) noexcept {
// std::remove_reference<T>::type: Remove reference from T
// If T is int& → int
// If T is int&& → int
//
// static_cast<...&&>: Cast to rvalue reference
// This casting causes the move constructor to be called
//
// Bottom line: std::move doesn't move, it just marks it as "movable"
// Actual movement is performed by the movement constructor/assignment operator
return static_cast<typename std::remove_reference<T>::type&&>(arg);
}
4. Rule of Five
#include <iostream>
class Resource {
private:
int* data;
public:
// 1. Constructor
Resource(int value) : data(new int(value)) {
std::cout << "constructor: " << *data << std::endl;
}
// 2. Destructor
~Resource() {
std::cout << "destructor: " << (data ? std::to_string(*data) : "null") << std::endl;
delete data;
}
// 3. Copy constructor
Resource(const Resource& other) {
data = new int(*other.data);
std::cout << "copy constructor: " << *data << std::endl;
}
// 4. Copy assignment operator
Resource& operator=(const Resource& other) {
if (this != &other) {
delete data;
data = new int(*other.data);
std::cout << "copy assignment: " << *data << std::endl;
}
return *this;
}
// 5. Move constructor
Resource(Resource&& other) noexcept {
data = other.data;
other.data = nullptr;
std::cout << "move constructor" << std::endl;
}
// 6. Move assignment operator
Resource& operator=(Resource&& other) noexcept {
if (this != &other) {
delete data;
data = other.data;
other.data = nullptr;
std::cout << "move assignment" << std::endl;
}
return *this;
}
int get() const { return data ? *data : 0; }
};
int main() {
Resource r1(10);
Resource r2 = r1;// copy constructor
Resource r3 = std::move(r1);// move constructor
Resource r4(20);
r4 = r2;// copy assignmentResource r5(30);
r5 = std::move(r4);// move assignment
return 0;
}
output of power:
Created by: 10
Copy Constructor: 10
move constructor
Created by: 20
Copy Assignment: 10
Created by: 30
transfer assignment
Destructor: 10
Destructor: 10
destructor: null
Destructor: 10
destructor: null
5. Practical example
Example 1: Vector optimization
#include <vector>
#include <iostream>
class BigObject {
private:
std::vector<int> data;
public:
BigObject(int size) : data(size, 0) {
std::cout << "Constructor: " << size << "elements" << std::endl;
}
BigObject(const BigObject& other) : data(other.data) {
std::cout << "Copy constructor: " << data.size() << "elements" << std::endl;
}
BigObject(BigObject&& other) noexcept : data(std::move(other.data)) {
std::cout << "Move constructor: " << data.size() << "elements" << std::endl;
}
};
std::vector<BigObject> createObjects() {
std::vector<BigObject> result;
result.push_back(BigObject(1000));// call move constructor
result.push_back(BigObject(2000));
return result;// RVO or move
}
int main() {
auto objects = createObjects();
std::cout << "complete" << std::endl;
return 0;
}
Example 2: Smart pointer movement
#include <memory>
#include <iostream>
void process(std::unique_ptr<int> ptr) {
std::cout << "Value: " << *ptr << std::endl;
}
int main() {
auto ptr1 = std::make_unique<int>(42);
// process(ptr1);// Error: unique_ptr cannot be copied
process(std::move(ptr1));// OK: Move
//ptr1 is now nullptr
if (!ptr1) {
std::cout << "ptr1 is empty" << std::endl;
}
return 0;
}
output of power:
Value: 42
ptr1 is empty
Example 3: Perfect Forwarding
#include <iostream>
#include <utility>
void process(int& x) {
std::cout << "lvalue version: " << x << std::endl;
}
void process(int&& x) {
std::cout << "rvalue version: " << x << std::endl;
}
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));// perfect delivery
}
int main() {
int x = 10;
wrapper(x);// call lvalue version
wrapper(20);// rvalue version call
return 0;
}
output of power:
lvalue version: 10
rvalue version: 20
6. Frequently occurring problems
Problem 1: Use after move
#include <vector>
#include <iostream>
int main() {
// ❌ Dangerous code
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);
std::cout << v1.size() << std::endl;// 0 (or undefined behavior)
//v1[0];// Undefined behavior!
// ✅ Correct code
std::vector<int> v3 = {4, 5, 6};
std::vector<int> v4 = std::move(v3);
// v3 is no longer used
return 0;
}
Problem 2: const objects cannot be moved
#include <vector>
#include <iostream>
int main() {
// ❌ No movement
const std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);// Copied!
std::cout << "v1 size: " << v1.size() << std::endl;// 3 (still valid)
// ✅ Without const
std::vector<int> v3 = {1, 2, 3};
std::vector<int> v4 = std::move(v3);// moved
std::cout << "v3 size: " << v3.size() << std::endl;// 0
return 0;
}
Issue 3: Exception in move constructor
#include <stdexcept>
#include <vector>
// ❌ Danger: noexcept
class Bad {
public:
Bad(Bad&& other) { // noexcept
throw std::runtime_error("error");
}
};
// ✅ Add noexcept
class Good {
std::vector<int> data;
public:
Good(Good&& other) noexcept : data(std::move(other.data)) {
//no exception thrown
}
};
int main() {
std::vector<Good> v;
v.reserve(10);// use noexcept move constructor
return 0;
}
Reason: When std::vector has a noexcept move constructor when reserve(), it is moved, otherwise it is copied.
Issue 4: Using std::move on return
#include <vector>
// Use ❌ move: Interrupt RVO
std::vector<int> createVector1() {
std::vector<int> v = {1, 2, 3};
return std::move(v);// RVO obstruction
}
// ✅ Just return: apply RVO
std::vector<int> createVector2() {
std::vector<int> v = {1, 2, 3};
return v;// RVO
}
int main() {
auto v1 = createVector1();
auto v2 = createVector2();
return 0;
}
7. Performance comparison
#include <chrono>
#include <vector>
#include <iostream>
void testCopy() {
std::vector<int> v1(1000000, 42);
auto start = std::chrono::high_resolution_clock::now();
std::vector<int> v2 = v1;// copy
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << "copy: " << duration.count() << "μs" << std::endl;
}
void testMove() {
std::vector<int> v1(1000000, 42);
auto start = std::chrono::high_resolution_clock::now();
std::vector<int> v2 = std::move(v1);// movement
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << "move: " << duration.count() << "μs" << std::endl;
}
int main() {
testCopy(); // ~1000μs
testMove(); // ~1μs
return 0;
}
result:
- Copy: ~1000μs (memory allocation + copy)
- Movement: ~1μs (copy only pointer)
8. Practice pattern
Pattern 1: Factory function
#include <vector>
#include <string>
class Database {
std::vector<std::string> data_;
public:
Database(std::vector<std::string> data)
: data_(std::move(data)) {} // move
size_t size() const { return data_.size(); }
};
// factory function
Database createDatabase() {
std::vector<std::string> data;
data.push_back("record1");
data.push_back("record2");
data.push_back("record3");
return Database(std::move(data));// movement
}
int main() {
Database db = createDatabase();// RVO or move
return 0;
}
Pattern 2: Container Optimization
#include <vector>
#include <string>
int main() {
std::vector<std::string> names;
// ❌ Copy
std::string name1 = "Alice";
names.push_back(name1);// copy
// ✅ Move
std::string name2 = "Bob";
names.push_back(std::move(name2)); // move
// ✅ emplace_back (better)
names.emplace_back("Charlie");// create directly
return 0;
}
Pattern 3: Swap Optimization
#include <vector>
#include <utility>
class Buffer {
std::vector<char> data_;
public:
Buffer(size_t size) : data_(size) {}
// move based swap
void swap(Buffer& other) noexcept {
data_.swap(other.data_); // O(1)
}
// or use std::swap
friend void swap(Buffer& a, Buffer& b) noexcept {
using std::swap;
swap(a.data_, b.data_);
}
};
int main() {
Buffer b1(1000), b2(2000);
swap(b1, b2);// fast travel
return 0;
}
9. Practical example: Resource Manager
#include <iostream>
#include <vector>
#include <memory>
#include <string>
class ResourceManager {
std::vector<std::unique_ptr<std::string>> resources_;
public:
// add resources
void add(std::unique_ptr<std::string> resource) {
resources_.push_back(std::move(resource)); // move
}
// Get resources
std::unique_ptr<std::string> take(size_t index) {
if (index >= resources_.size()) return nullptr;
auto resource = std::move(resources_[index]);// move
resources_.erase(resources_.begin() + index);
return resource;
}
// Number of resources
size_t count() const {
return resources_.size();
}
// Resource output
void print() const {
std::cout << "Resources (" << resources_.size() << "):" << std::endl;
for (size_t i = 0; i < resources_.size(); ++i) {
if (resources_[i]) {
std::cout << " [" << i << "]: " << *resources_[i] << std::endl;
} else {
std::cout << " [" << i << "]: (moved)" << std::endl;
}
}
}
};
int main() {
ResourceManager mgr;
// add resources
mgr.add(std::make_unique<std::string>("Resource 1"));
mgr.add(std::make_unique<std::string>("Resource 2"));
mgr.add(std::make_unique<std::string>("Resource 3"));
mgr.print();
// Get resources
auto r = mgr.take(1);
std::cout << "\nTook resource: " << *r << std::endl;
std::cout << "\nRemaining:" << std::endl;
mgr.print();
return 0;
}
output of power:
Resources (3):
[0]: Resource 1
[1]: Resource 2
[2]: Resource 3
Took resource: Resource 2
Remaining:
Resources (2):
[0]: Resource 1
[1]: Resource 3
organize
Key takeaways
- Move Semantics: Move resources without copying them
- rvalue reference: Binding temporary objects with
T&& - std::move: cast to rvalue (not actual move)
- noexcept: Required for move constructor/assignment
- Rule of Five: Destructor, copy, and move all implemented
Copy vs Move
| Features | Copy | Go |
|---|---|---|
| Cost | High (memory allocation + copy) | Low (pointer only) |
| Original | Maintenance | invalidation |
| Grammar | T a = b; | T a = std::move(b); |
| Generator | copy constructor | move constructor |
| College Admission | copy assignment | transfer admission |
| Use | Requires maintaining original | No original required |
Practical tips
Principle of use:
- When an object is no longer used
- When returning from a function (when RVO does not work)
- When inserting a container
- When transferring ownership (
unique_ptr)
Performance:
- Effective for dynamic memory objects
- Basic type has no effect
- RVO is faster than move
- Check with benchmark
caution:
- Do not use objects after moving
- const objects cannot be moved
- noexcept required for move constructor
- Do not use std::move on return
Next steps
- C++ Rvalue vs Lvalue
- C++ Perfect Forwarding
- C++ Copy Elision
Good article to read together (internal link)
Here’s another article related to this topic.
- C++ Rvalue vs Lvalue |“Value Categories” Guide
- C++ Perfect Delivery |“Perfect Forwarding” guide
- C++ Init Capture |“Init Capture” Guide
Practical tips
These are tips that can be applied right away in practice.
Debugging tips
- If you run into a problem, check the compiler warnings first.
- Reproduce the problem with a simple test case
Performance Tips
- Don’t optimize without profiling
- Set measurable indicators first
Code review tips
- Check in advance for areas that are frequently pointed out in code reviews.
- Follow your team’s coding conventions
Practical checklist
This is what you need to check when applying this concept in practice.
Before writing code
- Is this technique the best way to solve the current problem?
- Can team members understand and maintain this code?
- Does it meet the performance requirements?
Writing code
- Have you resolved all compiler warnings?
- Have you considered edge cases?
- Is error handling appropriate?
When reviewing code
- Is the intention of the code clear?
- Are there enough test cases?
- Is it documented?
Use this checklist to reduce mistakes and improve code quality.
Keywords covered in this article (related search terms)
This article will be helpful if you search for C++, move, rvalue, move semantics, performance, etc.
Related articles
- C++ Rvalue vs Lvalue |
- C++ Algorithm Copy |
- C++ std::function vs function pointer |
- C++ move error |
- C++ RVO·NRVO |