1. 핵심 개념
- Move Semantic
2. 상세 내용
2.1. C++ Value Categories


cpp에서의 식(expression)은 타입(type)과 식 범주(value category)라는 2개의 속성으로 특성을 부여할 수 있다. 식 범주는 lvalue, xvalue, prvalue 셋 중 하나로 나타난다.
- identity: 식별 가능한가
- moveable: 이동 가능한가(소유권)
위와 같이 2가지 기준을 바탕으로 다음과 같이 밸류를 나눌 수 있다.
- lvalue: 식별 가능하며(Identity O), 이동 불가능한(Movable X) 값. 메모리 주소상에 명확한 실체가 존재함.
- prvalue: 식별 불가능하며(Identity X), 이동 가능한(Movable O) 순수한 임시 값. 리터럴이나 연산 결과처럼 메모리상 고정된 실체가 없음.
- xvalue: 식별 가능하면서(Identity O), 동시에 이동도 가능한(Movable O) 값. 메모리에 실체는 존재하지만 소유권을 이전시킬 수 있어 rvalue의 일종으로 취급됨;
std::move(some_lvalue)의 결과 - glvalue (lvalue ∪ xvalue): '식별 가능한' 모든 값의 통칭.
- rvalue (prvalue ∪ xvalue): '이동 가능한' 모든 값의 통칭.
2.2. 사용 예시
생성자 정의
- 기본 생성자
- 복사 생성자: 복사할 원본 유지 + 깊은 복사
- 복사 대입 생성자: 기존 데이터 제거 + 복사할 원본 유지 + 깊은 복사
- 이동 생성자: 소유권 이전
- 이동 대입 생성자: 기존 데이터 제거 + 소유권이전
#include <iostream>
#include <utility> // std::move, std::swap
class MemoryBlock
{
private:
int* data;
size_t size;
public:
// 1. 기본 생성자
MemoryBlock(size_t s = 0) : size(s), data(s ? new int[s]() : nullptr)
{
std::cout << "기본 생성자 호출\n";
}
// 2. 소멸자
~MemoryBlock()
{
delete[] data;
std::cout << "소멸자 호출\n";
}
// 3. 복사 생성자 (원본 보존, 깊은 복사)
MemoryBlock(const MemoryBlock& other) : size(other.size), data(new int[other.size])
{
for (size_t i = 0; i < size; ++i)
{
data[i] = other.data[i];
}
std::cout << "복사 생성자 호출 (Deep Copy)\n";
}
// 4. 이동 생성자 (소유권 이전, 얕은 복사 후 원본 초기화)
// noexcept: 이동 중에는 예외가 발생하지 않음을 컴파일러에 보증 (STL 최적화에 필수)
// 만약 예외 나면 자비없이 std::terminate()의 대가를 치른다 ㄷㄷ
MemoryBlock(MemoryBlock&& other) noexcept : size(other.size), data(other.data)
{
other.size = 0;
other.data = nullptr; // 원본의 포인터를 끊어 이중 해제 방지
std::cout << "이동 생성자 호출 (Move)\n";
}
// 5. 복사 대입 연산자 (기존 자원 버리고, 깊은 복사)
MemoryBlock& operator=(const MemoryBlock& other)
{
if (this != &other)
{ // 자기 자신에 대한 대입 방지 (A = A;)
delete[] data; // 내 기존 자원 버리기
size = other.size;
data = new int[size];
for (size_t i = 0; i < size; ++i)
{
data[i] = other.data[i];
}
std::cout << "복사 대입 연산자 호출\n";
}
return *this;
}
// 6. 이동 대입 연산자 (기존 자원 버리고, 소유권 뺏어오기)
MemoryBlock& operator=(MemoryBlock&& other) noexcept
{
if (this != &other)
{
delete[] data; // 1. 내 기존 자원 버리기
data = other.data; // 2. 자원 가로채기
size = other.size;
other.data = nullptr; // 3. 원본 무력화
other.size = 0;
std::cout << "이동 대입 연산자 호출\n";
}
return *this;
}
};
int main()
{
MemoryBlock mb = MemoryBlock(); // 기본 생성자 호출
MemoryBlock mb2 = MemoryBlock(mb); // 복사 생성자 호출 - 원본 유지 깊은 복사
MemoryBlock mb3 = std::move(mb2); // 이동 생성자 호출 - 소유권 이전
mb3 = mb; // 복사 대입 생성자 호출 - 기존 자원 제거 + 원본 유지 깊은복사
mb3 = std::move(mb); // 이동 대입 생성자 호출 - 기존 자원 제거 + 소유권 이전
return 0;
}&& 타입으로 다루는 예시
&&는 int나 float처럼 독립적인 타입이 아니라, 포인터(*)나 좌측값 참조(&)처럼 기존 타입에 붙어서 특성을 부여하는 한정자이다.(rvalue만 가리킬 수 있는 변수)
오버로딩 길찾기 (가장 일반적인 용도)
컴파일러가 인자의 식 범주(Value Category)를 보고 알아서 최적의 함수를 찾아가게 만드는 용도
#include <iostream>
#include <string>
// lvalue를 받는 함수 (복사가 일어날 수 있음)
void process(const std::string& str) {
std::cout << "lvalue 처리 (복사용): " << str << "\n";
}
// rvalue를 받는 함수 (소유권을 탈취할 수 있음)
void process(std::string&& str) {
std::cout << "rvalue 처리 (이동용): " << str << "\n";
}
int main() {
std::string text = "Hello";
process(text); // text는 lvalue -> const std::string& 버전 호출
process(text + " World"); // 연산 결과는 임시값(prvalue) -> std::string&& 버전 호출
process(std::move(text)); // 강제로 xvalue로 캐스팅 -> std::string&& 버전 호출
return 0;
}지역 변수로서의 && 타입 (수명 연장)
우측값 참조 타입으로 지역 변수를 선언하면, 곧 사라질 임시 객체(prvalue)의 수명을 해당 변수의 스코프가 끝날 때까지 억지로 연장할 수 있다.
#include <iostream>
class Temp {
public:
Temp() { std::cout << "생성\n"; }
~Temp() { std::cout << "소멸\n"; }
};
Temp getTemp() {
return Temp(); // prvalue 반환
}
int main() {
std::cout << "--- 일반 호출 ---\n";
getTemp(); // 생성 -> 이 줄이 끝나자마자 즉시 소멸
std::cout << "--- && 타입으로 받기 ---\n";
// rRef라는 우측값 참조 '타입' 변수에 임시 객체를 바인딩
Temp&& rRef = getTemp();
std::cout << "아직 소멸 안됨!\n";
// main 함수가 끝날 때 비로소 rRef가 가리키는 객체가 소멸됨
return 0;
}템플릿에서의 && (보편 참조 / Forwarding Reference)
템플릿 매개변수 T에 &&가 붙으면, 이는 단순한 우측값 참조가 아니라 들어오는 값의 카테고리(lvalue인지 rvalue인지)를 그대로 보존하는 특수한 타입으로 바뀐다. (Perfect Forwarding)
예외적인 문법으로 템플릿에서 인자를 추론할 때 lvalue가 들어오면 T를 Type&(참조형)로, rvalue로 들어오면 T를 Type(일반형)으로 추론한다.
#include <iostream>
#include <utility>
template <typename T>
void wrapper(T&& arg) { // 여기서 T&&는 lvalue와 rvalue를 모두 받을 수 있는 마법의 타입입니다.
// 들어온 형태 그대로 다른 함수로 넘겨줌 (std::forward 사용)
process(std::forward<T>(arg));
}
[!NOTE] 학습 결론
생성자는 기본, 복사, 이동, 복사 대입, 이동 대입 5가지가 있고 이동생성자는 소유권의 이전을 나타내는 문법이다. &&를 받도록 정의된 메서드는 rvalue로 가져온 객체의 소유권을 받을 수 있음을 의미한다.
3. 질문 및 해결 (Q&A)
- 왜 move semantic 개념이 필요할까? 기존의 포인터를 명시적으로 이전하는 것으로도 충분히 move semantic의 개념을 수행할 수 있는데 문법적인 특성으로까지 분화한 이유가 뭘까?
-> 단순히 포인터의 할당과 해제를 떠나서 소유권의 이전 같은 구현을 언어 문법 차원에서 강제해서 그머가 포인터를 직접 다루지 않고 이동 생성자만(Move-Only) 열어두는 설계를 해서 안전하게 자원의 소권을 이동할 수 있게 함. 또한 컴파일러가 lvalue, rvalue를 구분함으로써 반환값의 깊은 복사 같은 문제를 컴파일러 차원에서 최적화할 수 있게 됨.
3.1. 참고
(1) lvalue와 prvalue의 구분 (복사 vs 자동 이동)
#include <iostream>
#include <string>
int main() {
std::string name = "C++"; // "C++"은 prvalue(리터럴), name은 lvalue(이름과 실체가 있음)
// 1. lvalue의 전달 -> '복사(Copy)' 발생
std::string copy_name = name;
// name은 이 줄 이후에도 계속 살아서 쓰여야 하므로(Movable X), 깊은 복사가 일어남.
// 2. prvalue의 전달 -> '이동(Move)' 발생
std::string temp_name = name + "11";
// (name + "11")의 연산 결과는 이름 없는 임시 객체(prvalue).
// 어차피 식을 넘어가면 소멸할 값이므로(Movable O), 컴파일러가 알아서 자원을 훔쳐옴.
return 0;
}(2) 함수 반환 시의 응용 (값 반환의 최적화)
#include <vector>
std::vector<int> makeHugeData() {
std::vector<int> temp(1000000, 1); // 100만 개짜리 벡터 생성 (lvalue)
return temp;
// 반환되는 순간 temp는 스코프를 벗어나며 소멸할 운명임.
// 최신 C++ 컴파일러는 이를 인지하고(temp를 xvalue처럼 취급) 100만 번의 깊은 복사 대신,
// 내부 데이터의 소유권(포인터)만 호출부로 던지는 '이동'을 수행해 성능 저하를 막아냄.
}
int main() {
std::vector<int> result = makeHugeData(); // 성능 저하 없이 100만 개 데이터 수신 완료
return 0;
}- STL 알고리즘 개선
// 두 객체의 값을 바꿀 때 (std::swap의 내부 구현)
template <typename T>
void swap(T& a, T& b) {
T temp = a; // C++98: 깊은 복사 발생
a = b; // C++98: 깊은 복사 발생
b = temp; // C++98: 깊은 복사 발생
}위 예시에서는 깊은 복사가 3번에 걸쳐 발생
template <typename T>
void swap(T& a, T& b) {
T temp = std::move(a); // 포인터 3번 이동으로 끝.
a = std::move(b); // 메모리 할당 0, 복사 0
b = std::move(temp);
}4. 관련 문서 (Links)
'내일배움캠프 > TIL' 카테고리의 다른 글
| TIL260604 - Unreal (0) | 2026.06.04 |
|---|---|
| TIL260602 - 개인 (0) | 2026.06.02 |
| TIL260529 (0) | 2026.05.29 |
| TIL260528 - 챕터2 프로젝트 - 2 (0) | 2026.05.28 |
| TIL260526 - CPP (0) | 2026.05.26 |