C++ Move Semantics: Copy vs Move Explained

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

  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

  • 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.


  • C++ Rvalue vs Lvalue |
  • C++ Algorithm Copy |
  • C++ std::function vs function pointer |
  • C++ move error |
  • C++ RVO·NRVO |