On this page
article
C++ Value Categories
Value Category의 기본 개념
C++에서 모든 표현식(expression)은 두 가지 특성을 가집니다:
- 타입(Type):
int
,double
,std::string
등 - 값 분류(Value Category): 표현식이 어떻게 메모리에 관련되는지 설명
전통적인 분류: lvalue와 rvalue
lvalue (left value)
- 정의: 메모리 상에 위치를 가지며, 식별자(identifier)로 참조할 수 있는 표현식
- 특징:
- 주소를 취할 수 있음 (
&
연산자 적용 가능) - 대입 연산자의 왼쪽에 올 수 있음 (이름의 유래)
- 지속성이 있는 값
- 주소를 취할 수 있음 (
- 예시:
int x = 10; // x는 lvalue int& ref = x; // ref는 lvalue reference
rvalue (right value)
- 정의: 임시적이고 식별자가 없는 표현식
- 특징:
- 일반적으로 주소를 취할 수 없음
- 대입 연산자의 오른쪽에만 올 수 있음
- 임시 값, 리터럴, 계산 결과 등
- 예시:
int y = 10 + 20; // 10 + 20은 rvalue int&& r_ref = 5; // r_ref는 rvalue reference
C++11 이후의 확장된 분류
5가지 값 범주
C++11에서는 더 정교한 값 분류 체계가 도입되었습니다:
- lvalue: 전통적 의미의 lvalue
- xvalue (eXpiring value): 수명이 끝나가는(만료되는) 객체에 대한 참조
- prvalue (Pure Rvalue): 순수한 임시 값
- glvalue (Generalized Lvalue): lvalue와 xvalue를 포함하는 상위 개념
- rvalue: xvalue와 prvalue를 포함하는 상위 개념
expression
/ \
glvalue rvalue
/ \ / \
lvalue xvalue prvalue
각 분류의 상세 설명
lvalue
- 특징: 지속적인 메모리 위치를 가지며 식별자로 참조 가능
- 예시:
int n = 5; // n은 lvalue int* p = &n; // p는 lvalue, &n은 rvalue *p = 10; // *p는 lvalue (역참조)
xvalue (eXpiring value)
- 특징:
- 이동 가능한 객체 (리소스를 빼앗길 수 있는 객체)
- 곧 소멸될 객체에 대한 참조
- 예시:
std::move(n) // std::move의 결과는 xvalue std::vector<int>{}.front() // 임시 객체의 멤버 접근은 xvalue static_cast<int&&>(n) // rvalue 캐스팅 결과는 xvalue
prvalue (Pure Rvalue)
- 특징:
- 순수한 임시 값으로 메모리 위치가 없음
- C++17부터 “임시 구체화(temporary materialization)“를 통해 xvalue로 변환 가능
- 예시:
42 // 리터럴은 prvalue n + 5 // 연산 결과는 prvalue []() { return 3; }() // 람다 표현식의 결과는 prvalue
glvalue (Generalized Lvalue)
- lvalue와 xvalue의 상위 개념
- 메모리에 위치가 있는 모든 표현식 (식별자로 직/간접 참조 가능)
rvalue
- xvalue와 prvalue의 상위 개념
- lvalue가 아닌 모든 표현식
값 범주의 의미와 필요성
왜 값 범주가 중요한가?
참조 바인딩 규칙:
T&
(lvalue reference)는 lvalue에만 바인딩 가능T&&
(rvalue reference)는 주로 rvalue에 바인딩 (단, 포워딩 참조 제외)
함수 오버로딩:
void foo(const T& x); // lvalue와 rvalue 모두 받음 void foo(T&& x); // rvalue만 받음 (오버로딩)
이동 의미론(Move Semantics):
- rvalue reference를 통해 객체의 내용을 효율적으로 이동
- 복사 비용 감소, 성능 향상
완벽한 전달(Perfect Forwarding):
- 템플릿에서 인자의 값 범주를 보존하여 전달
실용적 예시
이동 생성자 구현
class MyString {
char* data;
public:
// 이동 생성자
MyString(MyString&& other) noexcept
: data(other.data) {
other.data = nullptr; // 소유권 이전
}
};
// 사용
MyString s1{"Hello"};
MyString s2 = std::move(s1); // s1은 xvalue가 되어 이동 생성자 호출
표현식의 값 범주 결정 예제
int n = 10; // n은 lvalue
n = 20; // 대입 표현식의 결과 n은 lvalue
int& ref = n; // ref는 lvalue, 초기화에 lvalue인 n 사용
int&& rref = 42; // rref 자체는 lvalue, 초기화에 prvalue인 42 사용
n++; // 후위 증가는 prvalue 반환
++n; // 전위 증가는 lvalue 반환
immutable lvalue expression
정의: 수정할 수 없는 lvalue 표현식
예시:
const int c = 1; // c는 immutable lvalue c = 2; // 오류: const lvalue는 수정 불가
const
타입의 변수는 여전히 lvalue지만, 값 수정은 불가능합니다.메모리에 위치하고 식별자가 있어 lvalue이지만, 내용이 불변(immutable)입니다.
값 범주 판별 방법
decltype과 함께 사용
template<typename T>
void check_category(T&& x) {
if constexpr (std::is_lvalue_reference_v<decltype((x))>) {
std::cout << "lvalue expression\n";
} else {
std::cout << "rvalue expression\n";
}
}