본문으로 건너뛰기 C++ Move Semantics: Copy vs Move Explained

C++ Move Semantics: Copy vs Move Explained

C++ Move Semantics: Copy vs Move Explained

이 글의 핵심

C++11 move semantics: rvalue references, std::move, Rule of Five, noexcept move constructors—copy vs move performance and safe usage patterns.

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

  1. Move Semantics: Move resources without copying them
  2. rvalue reference: Binding temporary objects with T&&
  3. std::move: cast to rvalue (not actual move)
  4. noexcept: Required for move constructor/assignment
  5. Rule of Five: Destructor, copy, and move all implemented

Copy vs Move

FeaturesCopyGo
CostHigh (memory allocation + copy)Low (pointer only)
OriginalMaintenanceinvalidation
GrammarT a = b;T a = std::move(b);
Generatorcopy constructormove constructor
College Admissioncopy assignmenttransfer admission
UseRequires maintaining originalNo 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


Good article to read together (internal link)

Here’s another article related to this topic.

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

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.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. C++11 move semantics: rvalue references, std::move, Rule of Five, noexcept move constructors—copy vs move performance an… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, move semantics, rvalue reference, std::move, performance, Rule of Five 등으로 검색하시면 이 글이 도움이 됩니다.