1. lvalue와 rvalue
- lvalue : 메모리 주소를 가지고 있어 위치를 식별할 수 있는 객체로, 이름이 있고 계속 쓸 수 있는 값
- rvalue : 잠깐 쓰이고 사라지는 임시 값
int a = 10;
위 코드에서 a는 lvalue이고, 10은 rvalue이다. 대입문의 왼쪽에 오면 lvalue, 오른쪽에 오면 rvalue라고 봐도 대부분의 경우 틀리진 않지만 더 정확히는 이름을 통해 메모리 주소를 참조할 수 있으면 lvalue, 표현식이 끝나면 사라지는 일시적인 값은 rvalue다.
✓ lvalue와 rvalue의 구분이 중요한 이유는?
- 언제 복사가 발생하는지
- 언제 이동이 발생하는지
- 어떤 API 설계가 불필요한 복사를 줄이는지
명확히 알 수 있기 때문이다.
2. lvalue와 rvalue의 reference
- lvalue reference (T&) : lvalue에만 연결될 수 있는 참조 방식으로, 기존 객체에 별명을 붙이는 개념이다.
- rvalue reference (T&&) : C++11에서 도입되었으며, 임시 객체(rvalue)를 붙잡아 수명을 연장하거나 그 자원을 재사용(이동)할 수 있는 근거가 마련되었다.
예를 들어 어떤 함수가 const 파라미터를 받는다면, 읽기 전용으로 사용하겠다는 의도를 함수 시그니처로 표현한 것이다. 마찬가지로 함수 시그니처에 붙은 &와 &&는 이 함수가 인자로 받은 데이터를 빌려 쓰는건지, 혹은 자원을 뺏어가는건지를 포함한 API 사용 의도를 드러낸다.
아래와 같이 lvalue reference를 인자로 받는 함수가 있으면, "lvalue인 x는 계속 살아있는 객체이므로, 이 함수는 x를 소유하지 않는다. x의 수명은 호출자가 책임진다."는 의미를 내포한다.
void foo(T& x);
반면, rvalue reference를 인자로 받는 함수가 있으면, “x는 곧 사라질 객체이므로, 함수가 그 자원을 뺏어도(move) 된다”는 의미를 내포한다.
void foo(T&& x);
3. std::move의 역할
std::move는 이름과 달리 실제로 데이터를 '이동'시키지 않는다. 정확히는 lvalue를 rvalue로 캐스팅하는 역할만 하고, 실제 이동은 그 이후 호출되는 이동 생성자나 이동 대입연산자를 통해 이루어진다.
std::string a = "hello";
std::string b = std::move(a);
위 코드에선 a 객체가 더 이상 필요 없으니, 그 객체가 가진 자원(메모리 등)을 다른 곳으로 이동(Move Semantics)시켜도 좋다는 신호를 컴파일러에 전달한다.
그 결과 복사 생성자 대신 이동 생성자가 호출되고, 불필요한 깊은 복사(Deep Copy)를 방지해서 성능을 최적화 할 수 있다.
✓ std::move 이후의 객체 상태(moved from)
std::move로 자원을 옮긴 후, 원래의 객체는 유효하지만 내부 값은 보장되지 않는(valid but unspecified) 상태가 된다. 따라서 이동된 객체는 다시 초기화하기 전까지 사용하지 않도록 주의해야 한다.
#include <iostream>
#include <string>
int main() {
std::string a = "hello"; // 생성자
std::string b = a; // 복사 생성자
std::string c;
c = b; // 복사 대입 연산자
std::string d = std::move(c); // 이동 생성자
std::string e;
e = std::move(d); // 이동 대입 연산자
std::cout << a << std::endl;
std::cout << b << std::endl;
std::cout << c << std::endl; // valid but unspecified
std::cout << d << std::endl; // valid but unspecified
std::cout << e << std::endl;
}
// std::string의 경우엔 이동 후 비워지도록 구현 되어있다.
hello
hello
hello
4. return 값은 rvalue (RVO: Return Value Optimization)
리턴 값은 함수가 끝나면 사라지는 존재이므로 이미 rvalue로 취급된다.
또한 리턴 값을 받아서 객체를 생성하는 경우라면 모던 C++ 컴파일러는 RVO를 통해 복사나 이동조차 생략하고 객체를 목적지에 바로 생성한다. 따라서 return 시에는 std::move를 쓰면 오히려 RVO 최적화를 방해할 수 있으므로 쓰지 않도록 한다.
#include <iostream>
struct Obj {
Obj() {
std::cout << "생성자\n";
}
Obj(const Obj&) {
std::cout << "복사 생성자\n";
}
Obj(Obj&&) {
std::cout << "이동 생성자\n";
}
~Obj() {
std::cout << "소멸자\n";
}
};
Obj makeObj() {
Obj o;
return o;
}
int main() {
Obj o = makeObj();
}
생성자
소멸자
5. lvalue와 rvalue 이해가 API 설계로 이어지는 사례
✓ rvalue reference를 사용하는 과거의 setter 설계
// 1. lvalue 버전
void setName(const std::string& name) {
this->name = name;
}
// 2. rvalue 버전
void setName(std::string&& name) {
this->name = std::move(name);
}
- lvalue를 넘겼을 때, setName(str) : 1번 버전 함수 호출, 내부에서 복사 1회
- rvalue를 넘겼을 때, setName("John") : 2번 버전 함수 호출, 내부에서 이동 1회
이 설계는 복사든 이동이든 딱 필요한 작업만 1회 일어나지만, 오버로딩으로 코드량이 2배라서 setter가 많아지면 관리하기 힘들다는 단점이 있다.
✓ 모던 C++ 스타일 setter 설계
void setName(std::string name) {
this->name = std::move(name);
}
데이터를 내부 멤버에 저장(Sinking)하는 목적의 setter 함수라면 값으로 받고 내부에서 std::move를 쓰는 것이 요즘의 C++의 설계 방식이다.
- lvalue를 넘겼을 때, setName(str) : 지역 변수 name을 만들 때 복사 1회, 내부에서 이동 1회
- rvalue를 넘겼을 때, setName("John") : 임시 객체를 받아 지역 변수 name을 만들 때 이동 1회, 내부에서 이동 1회
이 설계의 장점은 오버로딩 없이 함수 하나만으로 lvalue, rvalue 둘 다 준수한 성능을 낸다는 것이다. 과거 설계보다는 이동이 1회씩 많지만 이동의 비용(포인터 복사 및 null 처리 등)은 복사에 비하면 거의 공짜라서 성능 체감이 크지 않다.

이처럼 API 설계에서 인자의 수명과 자원 소유에 대한 의도를 코드로 표현하기 위해서는, lvalue와 rvalue에 대한 이해가 필요하다.
※ 함수 시그니처가 주는 메시지
| 시그니처 형태 | lvalue 허용 | rvalue 허용 | 호출자에게 드러내는 설계 의도 |
| void func(const T& val) | O | O | 난 이 데이터를 구경만 할거임, 너가 가진 원본은 안전해 |
| void func(T& val) | O | X | 니가 준 원본 내가 수정할 수도 있다. 그러니 함수 호출이 끝나면 네 데이터는 변해 있을 수 있어 |
| void func(T&& val) | X | O | 어차피 너한테는 곧 버려질 대상이니, 내가 이 데이터 소유권을 가져갈 수도 있어 |
| void func(T val) | O | O | 니가 원본을 복사해 주든 임시 값을 주든 상관 없고, 이 데이터는 이제 내꺼니까 내가 알아서 할게 |
'프로그래밍 > C++' 카테고리의 다른 글
| C++ ] Overlapped IO 모델의 시리얼 통신 클래스 (0) | 2026.02.15 |
|---|---|
| C++ ] 템플릿을 활용한 thread-safe 링버퍼 클래스 (0) | 2026.02.05 |
| C++ ] 함수 템플릿, 클래스 템플릿 (0) | 2025.12.30 |
| C++ ] 싱글톤(Singleton) 클래스 (0) | 2025.12.15 |
| C++ ] constexpr (1) | 2025.11.30 |