C++을 공부하다 보면 std::move라는 표현을 자주 만나게 됩니다.
이름만 보면 객체를 어딘가로 “이동”시키는 함수처럼 보입니다.
하지만 실제로 std::move는 객체를 직접 이동시키지 않습니다.
조금 이상하게 들릴 수 있지만, 이 개념을 정확히 이해해야 C++의 move semantics, smart pointer, 성능 최적화, 객체 생명주기를 제대로 다룰 수 있습니다.
이번 글에서는 std::move가 실제로 무엇을 하는지, 그리고 실무에서 어떤 점을 조심해야 하는지 정리해보겠습니다.
1. lvalue와 rvalue 간단 정리
std::move를 이해하려면 lvalue와 rvalue를 알아야 합니다.
간단히 말하면 다음과 같습니다.
구분의미예시
| lvalue | 이름이 있고 다시 접근할 수 있는 값 | 변수 |
| rvalue | 임시 값, 곧 사라질 값 | 리터럴, 임시 객체 |
예를 들어:
int x = 10;
int y = x + 1;
여기서 x는 이름이 있는 변수이므로 lvalue입니다.
반면 x + 1은 계산 결과로 만들어진 임시 값이므로 rvalue입니다.
C++에서는 rvalue에 대해서 “어차피 곧 사라질 값이니까 내부 자원을 재사용해도 된다”고 판단할 수 있습니다.
이 아이디어가 move semantics의 핵심입니다.
2. std::move는 무엇인가?
먼저 결론부터 말하면 다음과 같습니다.
std::move는 객체를 이동시키는 함수가 아니라, 객체를 rvalue로 캐스팅하는 함수입니다.
즉, std::move 자체가 데이터를 옮기는 것은 아닙니다.
단지 컴파일러에게 이렇게 알려주는 역할을 합니다.
“이 객체는 이제 이동해도 괜찮은 값처럼 취급해줘.”
예를 들어 다음 코드를 보겠습니다.
#include <iostream>
#include <string>
#include <utility>
int main() {
std::string a = "hello";
std::string b = std::move(a);
std::cout << "a: " << a << '\n';
std::cout << "b: " << b << '\n';
}
이 코드에서 std::move(a)는 a를 직접 비우거나, b로 문자열을 복사해주는 함수가 아닙니다.
a를 rvalue처럼 취급하게 만들고, 그 결과 std::string의 move constructor가 호출될 수 있도록 도와줍니다.
3. 복사와 이동의 차이
다음과 같은 클래스가 있다고 가정해보겠습니다.
#include <iostream>
#include <vector>
class Data {
public:
std::vector<int> values;
Data() {
values = {1, 2, 3, 4, 5};
}
};
이 객체를 복사하면 내부의 vector 데이터도 새로 복사해야 합니다.
Data a;
Data b = a; // copy
하지만 이동은 다릅니다.
Data a;
Data b = std::move(a); // move
이 경우 a가 가지고 있던 내부 자원을 b가 가져갈 수 있습니다.
데이터를 새로 복사하는 것보다 훨씬 효율적일 수 있습니다.
특히 std::vector, std::string, std::unique_ptr처럼 내부에 동적 자원을 가진 객체에서는 move가 성능상 유리할 수 있습니다.
4. std::move는 이동을 보장하지 않는다
여기서 중요한 포인트가 있습니다.
std::move를 썼다고 해서 반드시 move가 일어나는 것은 아닙니다.
예를 들어 클래스에 move constructor가 없으면 어떻게 될까요?
class MyClass {
public:
MyClass() = default;
MyClass(const MyClass&) {
std::cout << "copy constructor\n";
}
};
이 클래스는 copy constructor만 가지고 있습니다.
MyClass a;
MyClass b = std::move(a);
이 경우 std::move(a)를 사용했지만 move constructor가 없기 때문에 copy constructor가 호출될 수 있습니다.
즉, std::move는 “이동해라”라는 명령이 아니라, “이동 가능한 값처럼 취급해도 된다”는 힌트에 가깝습니다.
실제로 이동을 수행하는 것은 해당 타입의 move constructor 또는 move assignment operator입니다.
5. 이동 후 객체는 어떻게 될까?
많이 하는 오해 중 하나는 다음입니다.
std::move를 사용하면 원본 객체는 무조건 비어 있다.
항상 그렇지는 않습니다.
std::string a = "hello";
std::string b = std::move(a);
이후 a는 유효한 상태입니다.
다만 값이 무엇인지는 타입의 구현에 따라 달라질 수 있습니다.
즉, move 이후의 객체는:
valid but unspecified state
라고 표현합니다.
한국어로 풀면:
객체는 여전히 파괴하거나 다시 대입할 수 있는 유효한 상태지만, 내부 값이 무엇인지는 기대하면 안 된다.
따라서 move 이후에는 원본 객체의 값을 읽어서 로직에 사용하는 것을 피하는 것이 좋습니다.
std::string a = "hello";
std::string b = std::move(a);
// 권장하지 않음
if (a.empty()) {
// 구현에 따라 결과가 달라질 수 있음
}
대신 move 이후의 객체는 다시 값을 대입해서 사용하는 것이 안전합니다.
a = "world";
6. std::move를 사용할 때 주의할 점
6-1. const 객체에는 move가 잘 안 된다
다음 코드를 보겠습니다.
const std::string a = "hello";
std::string b = std::move(a);
std::move(a)를 사용했지만, a는 const 객체입니다.
move는 원본 객체의 내부 자원을 가져오는 작업인데, const 객체는 수정할 수 없습니다.
따라서 이 경우 move가 아니라 copy가 발생할 가능성이 큽니다.
즉, const 객체에 std::move를 사용하는 것은 대부분 기대한 효과를 내지 못합니다.
6-2. return에서 무조건 std::move를 쓰지 말자
다음과 같은 코드도 자주 보입니다.
std::string createName() {
std::string name = "eunchan";
return std::move(name);
}
얼핏 보면 좋아 보이지만, 오히려 최적화를 방해할 수 있습니다.
현대 C++ 컴파일러는 return value optimization, 즉 RVO를 통해 불필요한 복사를 제거할 수 있습니다.
따라서 보통은 이렇게 쓰는 것이 좋습니다.
std::string createName() {
std::string name = "eunchan";
return name;
}
반환할 때 무조건 std::move를 붙이는 습관은 피하는 것이 좋습니다.
6-3. move 이후 객체를 다시 사용하지 말자
다음 코드는 위험한 습관입니다.
std::vector<int> a = {1, 2, 3};
std::vector<int> b = std::move(a);
std::cout << a.size() << '\n';
이 코드가 항상 크래시 나는 것은 아닙니다.
하지만 a의 상태를 기대하고 사용하는 것은 좋지 않습니다.
move 이후 객체는 소멸시키거나, 다시 대입해서 사용하는 정도로 생각하는 것이 안전합니다.
a = {4, 5, 6}; // 다시 대입 후 사용
7. unique_ptr에서 std::move가 필요한 이유
std::unique_ptr은 복사가 불가능합니다.
#include <memory>
std::unique_ptr<int> p1 = std::make_unique<int>(10);
std::unique_ptr<int> p2 = p1; // 컴파일 에러
왜냐하면 unique_ptr은 하나의 객체만 자원을 소유해야 하기 때문입니다.
소유권을 넘기고 싶다면 std::move를 사용해야 합니다.
std::unique_ptr<int> p1 = std::make_unique<int>(10);
std::unique_ptr<int> p2 = std::move(p1);
이제 p2가 자원을 소유하고, p1은 더 이상 해당 자원을 소유하지 않습니다.
이 예시는 std::move가 단순 성능 최적화뿐 아니라, 소유권 이전을 표현하는 데에도 사용된다는 것을 보여줍니다.
8. 실무에서는 어떻게 봐야 할까?
실무에서 std::move는 단순히 “복사보다 빠른 문법”이 아닙니다.
다음과 같은 의미를 가집니다.
- 이 객체의 자원을 더 이상 현재 위치에서 사용하지 않겠다.
- 소유권을 다른 객체로 넘겨도 된다.
- 불필요한 복사를 피하고 싶다.
- move 이후 원본 객체의 값에 의존하지 않겠다.
그래서 std::move를 사용할 때는 항상 스스로 질문해보는 것이 좋습니다.
이 객체를 정말 더 이상 사용하지 않을 것인가?
이 질문에 확실히 “그렇다”고 답할 수 있을 때 std::move를 사용하는 것이 좋습니다.
9. 정리
std::move는 C++에서 매우 자주 사용되지만, 이름 때문에 오해하기 쉬운 기능입니다.
핵심만 정리하면 다음과 같습니다.
- std::move는 객체를 직접 이동시키지 않는다.
- std::move는 객체를 rvalue로 캐스팅한다.
- 실제 이동은 move constructor 또는 move assignment operator가 수행한다.
- move 이후 객체는 유효하지만 값은 기대하면 안 된다.
- const 객체에 std::move를 써도 move가 안 될 수 있다.
- return에서 무조건 std::move를 쓰는 것은 좋지 않다.
- unique_ptr처럼 소유권 이전이 필요한 타입에서는 매우 중요하다.
결국 std::move는 성능 최적화 문법이면서 동시에 소유권 이전의 의도를 표현하는 도구입니다.
C++을 실무에서 다루려면 단순히 문법을 외우기보다, 객체의 생명주기와 소유권이 어떻게 이동하는지를 이해하는 것이 중요합니다.
'C++ > Concepts' 카테고리의 다른 글
| C++ unordered_map bucket과 rehash 이해하기 (0) | 2026.07.02 |
|---|---|
| C++ optional, 언제 쓰고 언제 피할까 (0) | 2026.06.29 |
| std::shared_ptr과 std::weak_ptr 제대로 이해하기 (0) | 2026.06.22 |
| std::unique_ptr 제대로 이해하기 (0) | 2026.06.18 |
| RAII로 자원 관리 이해하기 (0) | 2026.06.15 |