Value Category의 기본 개념

C++에서 모든 표현식(expression)은 두 가지 특성을 가집니다:

  1. 타입(Type): int, double, std::string
  2. 값 분류(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에서는 더 정교한 값 분류 체계가 도입되었습니다:

  1. lvalue: 전통적 의미의 lvalue
  2. xvalue (eXpiring value): 수명이 끝나가는(만료되는) 객체에 대한 참조
  3. prvalue (Pure Rvalue): 순수한 임시 값
  4. glvalue (Generalized Lvalue): lvalue와 xvalue를 포함하는 상위 개념
  5. 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가 아닌 모든 표현식

값 범주의 의미와 필요성

왜 값 범주가 중요한가?

  1. 참조 바인딩 규칙:

    • T& (lvalue reference)는 lvalue에만 바인딩 가능
    • T&& (rvalue reference)는 주로 rvalue에 바인딩 (단, 포워딩 참조 제외)
  2. 함수 오버로딩:

    void foo(const T& x);  // lvalue와 rvalue 모두 받음
    void foo(T&& x);       // rvalue만 받음 (오버로딩)
  3. 이동 의미론(Move Semantics):

    • rvalue reference를 통해 객체의 내용을 효율적으로 이동
    • 복사 비용 감소, 성능 향상
  4. 완벽한 전달(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";
    }
}