Hacking Modern C++ #1
17 Nov 2025
Move Semantics & Rvalue Reference: 등장 배경부터 실전 활용까지
C++11은 성능을 희생하지 않으면서 더 안전한 코드를 작성할 수 있도록 여러 기능을 도입했다. 그중에서도 Move Semantics와 Rvalue Reference(
T&&)는 현대 C++에서 가장 중요한 변화 중 하나다.
특히 컨테이너, 문자열, 대용량 객체에서 불필요한 복사를 제거하여 성능을 극적으로 개선한다.이 글에서는 그 등장 배경부터 실제 코드 예제까지 한 흐름으로 정리한다.
1. 등장 배경: 불필요한 복사 비용과 자원 관리의 문제
1) 복사(Copy)의 비용 증가
C++98 시대까지 객체는 기본적으로 “복사(copy)” 중심으로 동작했다.
문제는 대용량 데이터 구조(Vector, String, Buffer 등)가 많은 현대 응용 프로그램에서는 복사가 매우 비쌌다는 점이다.
예시로
std::vector를 리턴하는 코드를 생각해보자.std::vector<int> createVec() {
std::vector<int> v(1000000); // 매우 큰 벡터
return v; // C++98에서는 복사가 발생(복제 비용 매우 큼)
}
C++98에서는 RVO(Return Value Optimization) 같은 최적화에 의존해야 했고,
컴파일러가 최적화하지 못하면 불필요한 복사 비용이 발생했다.
2) 자원 독점 객체의 복사 금지 문제
복사가 비싼 것뿐 아니라 복사할 수 없는 객체도 있었다.
예:
- 파일 디스크립터 핸들(wrapper)
- 소켓 객체
- mutex 같은 OS 리소스
- unique ownership 패턴 객체
이런 객체는 “복사는 안 되지만 이동은 가능”해야 한다.
기존 C++98에서는 이를 지원할 언어적 수단이 없었다.
3) 해결책: “복사하지 말고 이동하자”
복사는 비용이 크고, 자원 소유권은 하나만 필요하다.
따라서 불필요한 데이터 복사를 피하고, 자원 소유권을 이전하는 기술이 필요해졌다.
그래서 도입된 것이 바로:
- Rvalue Reference (
T&&): 임시 객체에 대한 참조 - Move Semantics: 자원 소유권을 “복사”가 아닌 “이동”하도록 하는 규칙
2. Rvalue Reference & Move Semantics 동작 원리
1) Lvalue와 Rvalue의 구분
C++11은 기존 의미를 더 강화했다.
- Lvalue : 이름이 있고, 주소를 취할 수 있으며 재사용 가능한 값
- Rvalue : 임시적인 값(리터럴, 임시 객체 등)
예:
int a = 10; // a는 lvalue
int b = a + 3; // (a + 3)은 rvalue
2) Rvalue Reference: T&&
Rvalue reference는 오직 임시 객체(rvalue)에만 바인딩될 수 있는 참조 유형이다.
void func(int&& x); // rvalue만 받을 수 있음
func(10); // OK
int a = 5;
func(a); // ERROR: a는 lvalue
→ 의미: “이 객체는 곧 사라질 것이니 마음껏 훔쳐(steal) 써라”라는 계약
3) Move Constructor & Move Assignment
클래스가 자원을 소유하고 있다면 다음 두 연산을 정의할 수 있다.
MyClass(MyClass&& other) noexcept; // Move ctor
MyClass& operator=(MyClass&& other) noexcept; // Move assign
내부적으로 “자원 포인터만 넘기고 기존 포인터를 null 처리” 같은 방식으로 소유권을 이동한다.
예:
MyClass(MyClass&& other) noexcept
: data(other.data)
{
other.data = nullptr; // 원본은 비워 둔다
}
결과:
- 복사보다 훨씬 빠름
- 자원 중복 소유 문제 해결
3. 실제 예제: Move Semantics가 성능을 바꾸는 순간
1) 기본적인 Move Constructor
class Buffer {
public:
Buffer(size_t size)
: size(size), data(new int[size]) {}
// Move Constructor
Buffer(Buffer&& other) noexcept
: size(other.size), data(other.data)
{
other.size = 0;
other.data = nullptr;
}
~Buffer() { delete[] data; }
private:
size_t size;
int* data;
};
이제
Buffer 객체는 “복사할 수 없지만 이동할 수 있는” 현대적 구조가 된다.2) vector가 move를 활용하는 예
std::vector<Buffer> vec;
vec.push_back(Buffer(1024)); // 임시 객체 => move 발생
C++98에서는
- 임시 Buffer 생성 → 내부 배열 1024개 복사 → vec에 저장
C++11에서는
- 임시 Buffer 생성 → 내부 포인터만 이동 → 복사 비용 0
→ 큰 객체일수록 효과가 극대화됨
3) std::move
std::move는 lvalue를 강제로 rvalue로 캐스팅하기 위한 유틸리티다.Buffer a(1024);
Buffer b = std::move(a); // a의 자원이 b로 이동
주의:
std::move는 이동을 “보장”하지 않는다 → 단지 rvalue로 캐스팅할 뿐, 실제 이동 여부는 클래스의 move ctor가 결정
4. 사용 시 주의사항
1) 이동 후 객체는 “유효하지만 정의되지 않은 상태”
이동 후 객체는 다음이 보장된다:
- 소멸자 호출이 가능해야 함
- 다시 유효한 값으로 재할당 가능해야 함
그러나 내부 자원은 비어 있을 수 있으며 값을 읽으면 안 된다.
잘못된 예:
Buffer a(1024);
Buffer b = std::move(a);
size_t s = a.size; // 어떤 값인지 보장되지 않음 (사용 금지)
2) std::move의 남용 금지
다음과 같은 패턴은 성능을 해칠 수 있음:
std::string func(std::string s) {
return std::move(s); // 불필요 (NRVO 최적화를 깨뜨림)
}
불필요하게 move를 쓰면 컴파일러의 RVO/NRVO를 방해할 수 있다.
3) 복사/이동 연산의 강결합 주의
Move ctor를 정의하면 다음도 고려해야 한다:
- 복사 생성자/대입 연산자도 적절히 정의하거나
= default처리 - 예외 안전성 (
noexcept) 명시 → move 과정에서 예외 가능성이 있으면 컨테이너가 “복사”로 fallback함
4) self-move 금지
다음은 잘못된 패턴이다.
obj = std::move(obj); // 위험
자기 자신을 이동하는 내용은 예상치 못한 상태를 만들 수 있으므로 금지.
5. 정리
Move Semantics와 Rvalue Reference는 C++11이 가져온 가장 핵심적인 변화다.
그 필요성은 명확하다:
- 불필요한 복사를 제거하여 성능 향상
- 자원 소유 객체의 안전한 이동
- 컨테이너 동작 효율 극대화
이 개념을 이해하고 올바르게 사용하면, 현대 C++이 왜 아직도 성능 중심 시스템 개발에서 강력한 선택지인지 체감할 수 있을 것이다.