개요
안녕하세요. 이번 글에서는 C++11에서 도입된 이동 시맨틱(move semantics)에 대해 알아보겠습니다. 기존 C++에서는 객체를 다른 곳에 넘길 때 항상 복사가 일어났습니다. 그런데 곧 사라질 임시 객체까지 굳이 복사하는 것은 낭비입니다. 이동 시맨틱은 이런 경우 내용을 복사하는 대신 소유권만 넘겨서 불필요한 비용을 줄이는 기능입니다. 이를 이해하려면 먼저 lvalue와 rvalue 개념을 알아야 합니다.
lvalue와 rvalue
간단히 말하면 이름이 있어 계속 남는 값이 lvalue, 식이 끝나면 사라지는 임시 값이 rvalue입니다.
| 구분 | 설명 | 예 |
|---|---|---|
| lvalue | 이름이 있고 주소를 가지며 계속 존재하는 값 | int a; 에서 a |
| rvalue | 식이 끝나면 사라지는 임시 값 | 10, a + b, 함수의 반환 임시값 |
C++11부터는 rvalue를 가리키는 rvalue 참조(&&)가 생겼습니다. 이 덕분에 “이건 곧 사라질 임시 값이니 마음껏 가져가도 된다”는 것을 함수에 알려줄 수 있게 되었습니다.
void func(int& x) { /* lvalue 참조: 이름 있는 값을 받음 */ }
void func(int&& x) { /* rvalue 참조: 임시 값을 받음 */ }
int a = 10;
func(a); // lvalue → int& 버전 호출
func(20); // rvalue → int&& 버전 호출
std::move
std::move는 이름 그대로 “옮기는” 함수처럼 보이지만, 실제로는 아무것도 옮기지 않습니다. 단지 lvalue를 rvalue처럼 취급하도록 형변환(cast)해 줄 뿐입니다. 즉 “이 값은 이제 안 쓸 거니 가져가도 된다”고 표시하는 역할이며, 실제 이동은 뒤에서 설명할 이동 생성자가 담당합니다.
#include <string>
#include <utility> // std::move
std::string s1 = "hello";
std::string s2 = std::move(s1); // s1의 내용을 s2로 이동
// 이동 후 s1은 "비어 있는(유효하지만 미지정)" 상태가 됨
// 더 이상 s1의 값에 의존하면 안 됨
주의할 점은, 이동한 뒤의 원본(s1)은 비어 있는 상태가 된다는 것입니다. 값이 남아 있다고 가정하고 쓰면 안 됩니다.
복사 생성자 vs 이동 생성자
자원(메모리 등)을 소유하는 클래스를 예로 보겠습니다. 복사 생성자는 자원을 통째로 새로 복제하지만, 이동 생성자는 포인터만 넘겨받고 원본은 비웁니다. 그래서 훨씬 빠릅니다.
class Buffer
{
int* data;
size_t size;
public:
// 복사 생성자: 자원을 새로 복제 (비쌈)
Buffer(const Buffer& other) : size(other.size)
{
data = new int[size];
std::copy(other.data, other.data + size, data);
}
// 이동 생성자: 포인터만 가져오고 원본은 비움 (쌈)
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size)
{
other.data = nullptr; // 원본이 소멸돼도 이 메모리를 건드리지 않게
other.size = 0;
}
~Buffer() { delete[] data; }
};
복사는 배열 전체를 새로 만들어 값을 옮기지만, 이동은 이미 있는 메모리의 포인터만 넘겨받고 원본을 nullptr로 비웁니다. 데이터가 클수록 이 차이가 큽니다.
언제 자동으로 이동이 일어나는가
대부분의 경우 직접 std::move를 쓰지 않아도 컴파일러가 알아서 이동을 사용합니다. 대표적으로 rvalue(임시 객체)를 넘길 때입니다.
- std::vector 등 컨테이너에 임시 객체를 push_back 할 때
- 함수가 지역 객체를 return 할 때
- unique_ptr처럼 복사가 안 되고 이동만 되는 타입을 넘길 때
std::vector<Buffer> v;
v.push_back(Buffer()); // 임시 객체 → 이동 생성자 호출
std::unique_ptr<int> p1 = std::make_unique<int>(10);
std::unique_ptr<int> p2 = std::move(p1); // 이동만 가능 → 명시적 std::move
정리
- lvalue는 이름 있는 값, rvalue는 곧 사라질 임시 값입니다.
- std::move는 실제로 옮기는 게 아니라 lvalue를 rvalue로 취급하게 만드는 형변환입니다.
- 실제 이동은 이동 생성자/이동 대입이 담당하며, 복사와 달리 자원을 복제하지 않고 소유권만 넘깁니다.
- 임시 객체 전달, 지역 객체 반환 등에서는 컴파일러가 자동으로 이동을 사용합니다.
이동 시맨틱은 불필요한 복사를 줄여 성능을 높이는 C++11의 핵심 기능입니다. std::move는 “가져가도 된다”는 표시일 뿐, 실제 이동 동작은 이동 생성자가 정의한다는 점을 기억하면 이해가 쉽습니다. 이상으로 이동 시맨틱에 대해 간단히 알아보았습니다.