# **std::move는 실제로 이동하지 않는다? C++ 값 범주(Value Categories)를 쉽게 풀어보는 깊이 있는 탐구**

*SEO 키워드: `std::move`, `값 범주`, `C++ 이동语义`*

---

# # 📌 서론 – “왜 std::move가 움직이지 않는 것처럼 보일까?”

C++ 프로그래머라면 한 번쯤 **“std::move가 실제로 객체를 복사하거나 이동시키는 건가?”** 라는 의문을 품어본 적이 있을 것이다. 특히 **초보자**는 `std::move`를 호출하면 자동으로 메모리 복사가 일어나고, 그 결과가 바로 “이동”이라고 착각하기 쉽다. 하지만 **표준 라이브러리 구현부**를 살펴보면 `std::move`는 **그저 값 범주(Value Category)를 바꾸는 캐스팅 연산**에 불과하다.

이 글에서는

1. `std::move`가 실제로 하는 일

2. C++의 **값 범주**(lvalue, xvalue, prvalue)와 그 의미

3. 이동 생성자·이동 대입 연산자가 언제 호출되는지

를 차근차근 풀어낸다. 코드를 직접 실행해 보며 **“이동이 일어나지 않았다”**는 상황을 재현하고, 올바른 사용법까지 정리한다.

---

# # 🔎 본론

## # 1️⃣ `std::move`는 “값을 이동한다”가 아니라 “값을 **우측값**(rvalue)으로 변환한다”

```
cpp

template< class T >

constexpr typename std::remove_reference<T>::type&& move( T&& t ) noexcept {

    return static_cast<typename std::remove_reference<T>::type&&>(t);

}

```

위 구현을 보면 알 수 있듯이 `std::move`는 **`static_cast`** 를 이용해 **`T&&`** 로 변환한다.

- **입력**: `T&& t` – 여기서 `t`는 **전달된 인수의 실제 형식**에 따라 `lvalue reference` 혹은 `rvalue reference**가 될 수 있다.

- **출력**: `remove_reference<T>::type&&` – `T`에서 `&`를 제거하고 다시 **우측값 참조**(`&&`)를 만든다.

즉, `std::move`는 **객체 자체를 복사하거나 메모리를 이동시키지 않는다**. 단순히 **“이 객체를 이제는 우측값으로 취급하라”**는 신호를 컴파일러에 전달한다. 실제 이동이 일어나려면 **이 우측값을 매개변수로 받는 함수(대개 이동 생성자·이동 대입 연산자)**가 호출돼야 한다.

> **핵심 포인트**

> - `std::move`는 **컴파일 타임**에만 작동한다. 런타임 동작을 바꾸지는 않는다.

> - `std::move` 뒤에 바로 `cout << obj;` 같은 코드를 써도 객체는 그대로 남아 있다.

---

## # 2️⃣ 값 범주(Value Categories) – C++가 객체를 어떻게 구분하는가

C++에서는 **표현식(expression)** 이 **어떤 상황(lvalue, rvalue)** 에서 사용되는지에 따라 **값 범주**가 정해진다. 주요 3가지 범주는 다음과 같다.

| 값 범주 | 정의 | 특징 | 예시 |

|--------|------|------|------|

| **lvalue** | 메모리 주소를 가지고 있는 식 | 이름이 있는 변수, 참조 등 | `int a; a` |

| **xvalue** (expiring value) | **이동 가능한** 우측값, 즉 “소멸될(소유권이 이전될) 가능성”이 있는 값 | `std::move(a)`, `std::forward<T>(t)` 등 | `std::move(str)` |

| **prvalue** (pure rvalue) | 임시 객체, 리터럴 등 **소유권이 없는** 값 | `42`, `std::string('tmp')` | `int(5)` |

> **왜 xvalue가 중요한가?**

> xvalue는 **“이제 이 객체는 더 이상 사용되지 않을 것이니 소유권을 넘겨도 된다”** 라는 의미를 내포한다. 이동 생성자·이동 대입 연산자는 **xvalue** 를 인자로 받을 때만 호출된다.

### # 2‑1️⃣ lvalue → xvalue 로 변환하는 방법

- `std::move` : 가장 일반적인 방법.

- `std::forward<T>` : 템플릿 인수 `T`가 `lvalue`인지 `rvalue`인지 보존하면서 전달.

### # 2‑2️⃣ 값 범주가 함수 오버로드에 미치는 영향

```
cpp

void foo(const std::string& s);   // lvalue 전용

void foo(std::string&& s);        // rvalue 전용 (xvalue 포함)

std::string s = 'hello';

foo(s);               // 첫 번째 overload 호출 (lvalue)

foo(std::move(s));    // 두 번째 overload 호출 (xvalue)

```

위 예시처럼 **오버로드된 함수**는 **값 범주에 따라 다르게 동작**한다. 이는 **성능 최적화**와 **자원 관리**에 핵심적인 역할을 한다.

---

## # 3️⃣ 이동 생성자·이동 대입 연산자는 언제 호출되는가

| 상황 | 호출되는 함수 | 왜 호출되는가 |

|------|--------------|---------------|

| `T t = std::move(src);` | **이동 생성자** | `src`가 xvalue이므로 “새 객체를 만들면서 src의 자원을 훔쳐온다”. |

| `t = std::move(src);`   | **이동 대입 연산자** | 기존 객체 `t`에 src의 자원을 “재사용”한다. |

| `func(std::move(src));` | **이동 생성자** (매개변수가 값) 또는 **이동 대입 연산자** (매개변수가 참조) | 함수 인자 전달 시 우측값으로 변환된 `src`가 이동 대상이 된다. |

### # 3‑1️⃣ 이동이 일어나지 않는 함정

```
cpp

std::vector<int> v1 = {1,2,3};

std::vector<int> v2 = v1;          // 복사 생성자 호출 → 비용 비쌈

std::vector<int> v3 = std::move(v1); // 이동 생성자 호출 → 비용 저렴

```

하지만 **다음과 같은 경우는 이동이 일어나지 않는다**:

```
cpp

void foo(std::vector<int> v);   // 값 전달 (복사 또는 이동)

std::vector<int> a = {1,2,3};

foo(a);               // a는 lvalue → 복사 생성자 호출

foo(std::move(a));    // a는 xvalue → **이동 생성자** 호출 (가능)

```

만약 함수가 **`const std::vector<int>&`** 로 받는다면 `std::move(a)`를 넘겨도 **복사가 일어나지 않는다**(오히려 const lvalue가 되므로 이동 불가).

### # 3‑2️⃣ 이동이 불가능한 경우

- **삭제된(move deleted) 이동 생성자**가 존재하거나

- **복사 생성자만 정의**된 클래스(예: `std::unique_ptr`은 복사 불가)

이때 `std::move`를 사용하면 **컴파일 오류**가 발생한다.

---

## # 4️⃣ 실전 예제 – `std::move`와 값 범주를 직접 확인해보자

```
cpp

# include <iostream>

# include <utility>

struct Demo {

    Demo() { std::cout << 'Default ctor\n'; }

    Demo(const Demo&) { std::cout << 'Copy ctor\n'; }

    Demo(Demo&&) noexcept { std::cout << 'Move ctor\n'; }

    ~Demo() { std::cout << 'Dtor\n'; }

};

int main() {

    Demo d1;                 // Default ctor

    Demo d2 = d1;            // Copy ctor

    Demo d3 = std::move(d1); // Move ctor

}

```

**출력**

```

Default ctor

Copy ctor

Move ctor

Dtor

Dtor

Dtor

```

- `d1` → `d2` : **복사**

- `std::move(d1)` → `d3` : **이동** (실제로 이동 생성자가 호출됨)

> **핵심 정리**

> - `std::move` 자체는 아무 동작도 하지 않는다.

> - **이동 생성자/대입 연산자**가 존재하고, **우측값(xvalue)** 로 전달될 때만 이동이 일어난다.

---

## # 5️⃣ `std::move`를 올바르게 쓰는 팁

| 상황 | 올바른 사용법 |

|------|--------------|

| **임시 객체를 반환** | `return std::move(local);` → **불필요**. 반환값 최적화(RVO) 때문에 자동 이동이 일어나므로 `std::move`를 제거한다. |

| **컨테이너에 원소 삽입** | `vec.push_back(std::move(obj));` → 객체를 **소유권을 넘겨** 삽입, 이후 `obj`는 비어있음. |

| **템플릿 함수에서 전달** | `template<class T> void wrapper(T&& arg) { func(std::forward<T>(arg)); }` – `std::move` 대신 `std::forward` 사용. |

| **const 객체** | `std::move(constObj)` → **xvalue**가 되지만 **const** 특성 때문에 이동 생성자를 호출할 수 없다. 복사만 일어남. |

---

# # 🎯 결론 – 행동으로 옮기는 `std::move` 이해하기

1️⃣ **`std::move`는 “값을 이동한다”가 아니라 “값을 우측값으로 만든다”**는 사실을 기억하자.

2️⃣ **값 범주**(lvalue, xvalue, prvalue)를 정확히 구분하면 언제 이동이 일어나고 언제 복사가 일어나는지 명확히 판단할 수 있다.

3️⃣ 실제 **이동 생성자·이동 대입 연산자**가 구현된 타입에만 `std::move`를 적용하고, **불필요한 `std::move`**(예: 반환값) 는 제거해 코드를 깔끔하게 유지한다.

> **당신에게 권하는 실천 과제**

> - 오늘 작성 중인 C++ 프로젝트에서 **`std::move` 사용 위치**를 한 번씩 점검해 보세요.

> - 이동이 가능한 클래스라면 **이동 생성자/대입 연산자를 직접 구현**하고, `std::move`와 값 범주를 테스트해 보는 연습을 해보세요.

이제 **“std::move는 실제로 움직이지 않는다”**는 오해를 벗고, **값 범주를 활용한 최적화**를 자신 있게 적용할 수 있을 것이다.

---

*읽어주셔서 감사합니다! 더 많은 C++ 팁과 최신 프로그래밍 트렌드를 원한다면 아래 구독 버튼을 눌러 주세요.*
