본문 바로가기

C++ lvalue, rvalue

@iamrain2025. 12. 1. 20:19

1. C++ lvalue, rvalue

C++에서 lvaluervalue는 표현식(expression)을 구분하는 중요한 카테고리입니다. 모든 표현식은 lvalue 또는 rvalue (또는 그 변형) 중 하나에 속합니다. 이 구분은 C++11에서 이동 의미론(move semantics)완벽 전달(perfect forwarding)이 도입되면서 더욱 중요해졌습니다.

  • lvalue (locator value):
    • 개념: lvalue는 식별 가능한(identifiable) 메모리 위치를 차지하는 표현식을 의미합니다. 이름이 있는 변수, 배열의 원소, 멤버 변수 등이 해당됩니다.
    • 특징:
      • 주소 연산자(&)를 적용하여 주소를 가져올 수 있습니다.
      • 대입 연산자(=)의 왼쪽에 올 수 있습니다. 즉, 값을 대입받을 수 있습니다.
      • 일반적으로 "지속적인(persistent)" 상태를 가집니다. 즉, 표현식이 평가된 후에도 그 값이 메모리에 남아있습니다.
    • 예시: int x = 10;, x는 lvalue. int arr[5];, arr[0]은 lvalue. std::string s;, s는 lvalue.
  • rvalue (read value):
    • 개념: rvalue는 lvalue가 아닌 모든 것을 의미합니다. 주로 임시적인(temporary) 값이나 리터럴(literal)을 가리킵니다.
    • 특징:
      • 식별 가능한 메모리 위치를 가지지 않습니다. (예외: string literal)
      • 주소 연산자(&)를 적용할 수 없습니다.
      • 대입 연산자(=)의 왼쪽에 올 수 없습니다.
      • 표현식이 평가되는 시점에만 존재하고, 그 이후에는 사라지는 임시 값입니다.
    • 예시: 10 (리터럴), x + 5, get_value() (값을 반환하는 함수 호출) 등.

C++11 이후 rvalue는 두 가지로 더 세분화됩니다.

  • prvalue (pure rvalue): 순수한 rvalue. 리터럴(e.g., 42, true), 임시 객체 등이 해당됩니다.
  • xvalue (eXpiring value): "곧 소멸될 값"을 의미합니다. 대표적으로 std::move()의 결과가 xvalue입니다. xvalue는 lvalue처럼 식별 가능한 위치를 가지지만, rvalue처럼 "이동(move)해도 되는" 자원으로 취급됩니다.

1.1 rvalue 참조와 이동 의미론 (Move Semantics)

C++11에서는 rvalue 참조(&&)가 도입되었습니다. 이는 rvalue를 바인딩할 수 있는 참조입니다.

int x = 10;
int&  lref = x;       // OK: lvalue 참조는 lvalue를 바인딩
// int&  lref2 = 10;  // Error: lvalue 참조는 rvalue를 바인딩할 수 없음
const int& lref3 = 10; // 예외: const lvalue 참조는 rvalue를 바인딩할 수 있음

int&& rref = 10;      // OK: rvalue 참조는 rvalue를 바인딩
// int&& rref2 = x;   // Error: rvalue 참조는 lvalue를 바인딩할 수 없음
int&& rref3 = std::move(x); // OK: std::move가 lvalue를 rvalue처럼 취급하게 함

rvalue 참조의 핵심 목적은 이동 의미론(Move Semantics)을 구현하는 것입니다. 이동 의미론은 임시 객체(rvalue)의 자원(예: 동적 할당된 메모리, 파일 핸들 등)을 불필요하게 복사하는 대신, "훔쳐오는" (이동하는) 최적화 기법입니다.

내부 구조 (이동 생성자 예시):

class MyString {
public:
    // 복사 생성자 (깊은 복사)
    MyString(const MyString& other) {
        _size = other._size;
        _data = new char[_size];
        std::copy(other._data, other._data + _size, _data);
    }

    // 이동 생성자 (자원 이동)
    MyString(MyString&& other) noexcept { // rvalue 참조를 인자로 받음
        // 1. 자원을 훔쳐온다 (얕은 복사)
        _data = other._data;
        _size = other._size;

        // 2. 원본 객체(other)를 안전한 상태로 만든다.
        //    (소멸자가 훔쳐간 자원을 해제하지 않도록)
        other._data = nullptr;
        other._size = 0;
    }
    // ...
private:
    char* _data;
    size_t _size;
};

MyString create_string() { return MyString("hello"); }

// 사용
MyString s1 = create_string(); // create_string()이 반환하는 임시 객체(rvalue)
                               // s1을 초기화할 때 이동 생성자가 호출됨.

create_string()이 반환하는 임시 MyString 객체는 rvalue입니다. 이 rvalue로 s1을 초기화할 때, 오버로딩 규칙에 따라 MyString(MyString&&) 이동 생성자가 선택됩니다. 이동 생성자는 임시 객체의 _data 포인터만 복사하고, 임시 객체의 포인터는 nullptr로 만들어버립니다. 비싼 메모리 복사 없이 자원의 소유권만 이전되므로 매우 효율적입니다.

2. 계층 구조별 동작

  1. 사용자 영역 (User Code): 개발자는 std::move를 사용하여 lvalue를 rvalue로 캐스팅함으로써 명시적으로 이동을 지시할 수 있습니다. 함수에서 값을 반환할 때 컴파일러는 종종 자동으로 이동을 수행합니다 (RVO - Return Value Optimization).
  2. 컴파일러 영역 (Compiler):
    • 표현식 분석: 모든 표현식을 lvalue, rvalue(prvalue, xvalue)로 분류합니다.
    • 오버로드 확인 (Overload Resolution): 함수 호출 시 인자의 값 카테고리(lvalue/rvalue)에 가장 잘 맞는 오버로딩된 함수를 선택합니다. 예를 들어, 인자가 rvalue이면 rvalue 참조(&&)를 받는 버전이 우선적으로 선택됩니다.
    • 최적화: RVO/NRVO(Named Return Value Optimization)를 통해 함수 반환 시 불필요한 복사나 이동을 아예 생략하기도 합니다.
  3. 실행 영역 (Runtime):
    • lvalue는 일반적으로 스택이나 힙에 명확한 주소를 가지고 존재합니다.
    • rvalue는 보통 CPU 레지스터에 임시로 저장되거나, 스택의 임시 공간에 잠시 존재했다가 사라집니다. 이동 의미론이 적용되면, 이 임시 객체의 내부 포인터나 핸들 값이 새로운 객체로 "이동" (단순 값 복사) 됩니다.

3. 요약

C++에서 모든 표현식은 lvalue 아니면 rvalue로 나뉩니다. 간단히 말해, lvalue는 이름이 있고 주소를 가질 수 있는, 계속해서 사용될 변수이고, rvalue는 리터럴이나 함수 반환 값처럼 곧 사라질 임시적인 값입니다. 예를 들어 int a = 10;에서 a는 lvalue, 10은 rvalue입니다.

C++11부터는 rvalue 참조라는 개념이 도입되어 이런 임시 값(rvalue)을 효율적으로 처리할 수 있게 되었습니다. 이를 통해 이동 의미론(Move Semantics)을 구현하는데, 이것은 임시 객체가 가진 자원을 비싸게 복사하는 대신, 그 자원의 소유권만 '훔쳐오는' 최적화입니다. 예를 들어, 함수가 큰 벡터를 반환할 때, 벡터의 내용을 전부 복사하는 게 아니라, 내부 데이터 포인터만 새 벡터 객체로 넘겨주어 성능을 크게 향상시킬 수 있습니다. std::move는 lvalue를 강제로 rvalue처럼 취급하게 만들어, 이러한 이동 최적화를 수동으로 유도할 때 사용합니다.

4. 실습 코드

#include <iostream>
#include <string>
#include <utility> // std::move를 위해 필요

// 데미지 정보를 담는 간단한 구조체
struct DamageInfo {
    int amount;
    std::string type;

    // 생성자
    DamageInfo(int amt, std::string t) : amount(amt), type(std::move(t)) {
        std::cout << "DamageInfo 일반 생성자 호출 (" << amount << ", " << type << ")" << std::endl;
    }

    // 복사 생성자: lvalue가 전달될 때 호출됨
    DamageInfo(const DamageInfo& other) : amount(other.amount), type(other.type) {
        std::cout << "DamageInfo 복사 생성자 호출!" << std::endl;
    }

    // 이동 생성자: rvalue가 전달될 때 호출됨
    DamageInfo(DamageInfo&& other) noexcept : amount(other.amount), type(std::move(other.type)) {
        std::cout << "DamageInfo 이동 생성자 호출!" << std::endl;
        other.amount = 0; // 원본 객체의 자원을 무효화
    }
};

// 게임 캐릭터 클래스
class Character {
public:
    std::string name;
    int health;

    // 캐릭터 생성자
    Character(std::string n, int h) : name(std::move(n)), health(h) {}

    // lvalue 참조를 받는 ApplyDamage 함수
    void ApplyDamage(const DamageInfo& damage) {
        health -= damage.amount;
        std::cout << name << "이(가) " << damage.type << " 데미지를 " << damage.amount << " 받았습니다. (lvalue 참조 버전) 남은 체력: " << health << std::endl;
    }

    // rvalue 참조를 받는 ApplyDamage 함수
    void ApplyDamage(DamageInfo&& damage) {
        health -= damage.amount;
        std::cout << name << "이(가) " << damage.type << " 데미지를 " << damage.amount << " 받았습니다. (rvalue 참조 버전) 남은 체력: " << health << std::endl;
    }
};

// 임시 DamageInfo 객체를 생성하여 반환하는 함수
DamageInfo CreateFireballDamage() {
    return DamageInfo(30, "화염");
}

int main() {
    // 플레이어 캐릭터 생성
    Character player("용사", 100);

    // 이름이 있는 lvalue 객체 생성
    DamageInfo poison_damage(10, "독");

    player.ApplyDamage(poison_damage);

    std::cout << "---" << std::endl;

    player.ApplyDamage(CreateFireballDamage());

    std::cout << "---" << std::endl;

    player.ApplyDamage(std::move(poison_damage));

    // std::move 이후 poison_damage의 상태 확인
    std::cout << "std::move 이후 poison_damage의 데미지 양: " << poison_damage.amount << std::endl;

    return 0;
}
iamrain
@iamrain :: Annals of Unreal

iamrain 님의 블로그 입니다.

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차