3 분 소요

lValue 와 rValue

‘값’에 대한 ‘분류’이며
일종의 ‘표현식’에 가깝다

lValue

  • 메모리에 주소를 가질 수 있는 값, ‘참조’ 가능한 값
    (&를 통해 ‘참조’ 가능)
    (ex : 변수, 배열의 원소 등)
int a = 10;    // a는 lvalue
int* p = &a;   // a는 메모리에 있으므로 주소를 얻을 수 있음

rValue

  • 임시 값이나 이름 없는 값 (수정하거나 참조 불가한 것들)
    (ex : 리터럴, 연산 결과 등)
int x = 1 + 2; // 1+2는 rvalue
x = x + 3;     // x는 lvalue, x+3은 rvalue
int* p = &(5 + 2); // ❌ 컴파일 에러 — rvalue는 주소가 없음
  • rvalue는 크게 2가지로 나뉜다
    • prValue(Pure Value) : 값 그 자체(42,”Hello”,SomeClaee())
    • xValue(Expiring Value) : 소유권을 잃을 예정인 값 (std::move(),std::string().substr(0,2))
      (xValue 자체는 ‘컴파일러’가 판단)

rValue Reference

개념

기존의 ‘참조’(T&)는 lVaue만 참조가 가능했음

int a = 10;
int& ref1 = a;     // OK
int& ref2 = 5;     // ❌ rvalue를 참조할 수 없음

C++ 11 부터 rValue Reference의 도입으로
rValue에 대한 참조가 가능해짐 (T&&)

int&& rref = 5; // OK: 5는 rvalue

포인터 처럼 ‘일종의 타입’으로 인식
(ex : int&&, string&&)

사용처

  • Move : 자원을 ‘복사’하는 것이 아닌 ‘이전’

  • Perfect Forwarding : 템플릿 함수에서 ‘전달받은 인자’의 값 속성(lValue/rValue)을 다른 함수에 전달 가능

필요한 이유??

  • 기존 참조가 rValue를 받을 수 없어서 ‘임시 객체’ 등에 대한 참조가 불가능
  • 임시 객체를 더 효율적으로 다루기 위함
    (큰 크기의 임시 변수를 생성하였을 때, 그에 대한 ‘복사’ 대신
    소유권을 이전함으로서 최적화)

이동(Move)

불필요한 복사를 피하고, 리소스를 ‘이동’시켜
성능 향상을 하기 위한 기능

std::Move()

std::string a = "text";
std::string b = std::move(a); // 이동 생성자 호출

std::move() 자체는 값을
rvalue로 캐스팅해준다(정확히는 xvalue)

이 때, string의 이동 생성자 / 이동 대입 연산자의 T&&가 선택되어
실제로 값을 옮기는 역할을 수행
(다만 보통 이 과정에서 원본의 데이터를 유효하지 않게 만듦)
(move 이후, 원본 값은 사용하지 않는걸 권장)

  • 이 때, a와 b는 ‘같은 클래스 타입’이여야 한다
    (정확히 말하자면 ‘업캐스팅’은 가능하지만 ‘다운 캐스팅’은 에러가 발생)
    (애초에 ‘다운 캐스팅’을 의도하였다면 그냥 캐스팅을 한 이후에 사용하자)
B b;
A a = std::move(b); // OK → **Object Slicing 발생**

→ A의 멤버만 남고, B 고유 멤버는 잘림

void takeA(A&&) { std::cout << "A&&\n"; }
void takeB(B&&) { std::cout << "B&&\n"; }

A a;
B b;

takeA(std::move(a)); // A&&
takeA(std::move(b)); // A&& (업캐스팅)
takeB(std::move(b)); // B&&
takeB(std::move(a)); // ❌ 컴파일 에러

  • TMI : string이 저장되는 위치는 어디?
    1. 리터럴인 “hello”라면 ‘읽기 전용 데이터 영역’에 그 값이 위치된다
    2. string(“Hello”) 를 통해 문자열 선언한 경우, 그 값을 복사하여 heap에 메모리 할당 된다
    3. 만약 위에서 a를 지역변수로 사용했다면 a의 메모리 자체는 스택에 위치

이동 생성자/ 이동 대입 연산자

항목 설명
이동 생성자 새 객체를 만들 때, 기존 객체의 자원을 훔쳐오기 위한 생성자
이동 대입 연산자 기존 객체에 다른 객체의 자원을 옮겨 심는 연산자

두 방식 모두 T&&(rvalue reference)를 매개 변수로 받음
(임시 객체 or std::move()를 통해 호출)

#include <iostream>
#include <string>

class MyClass {
public:
    std::string data;

    // 생성자
    MyClass(const std::string& s) : data(s) {
        std::cout << "Constructor\n";
    }

    // 복사 생성자
    MyClass(const MyClass& other) : data(other.data) {
        std::cout << "Copy Constructor\n";
    }

    // 이동 생성자
    MyClass(MyClass&& other) noexcept : data(std::move(other.data)) {
        std::cout << "Move Constructor\n";
    }

    // 복사 대입 연산자
    MyClass& operator=(const MyClass& other) {
        std::cout << "Copy Assignment\n";
        data = other.data;
        return *this;
    }

    // 이동 대입 연산자
    MyClass& operator=(MyClass&& other) noexcept {
        std::cout << "Move Assignment\n";
        data = std::move(other.data);
        return *this;
    }
};

당연하지만 class 내부에서 heap과 연관된 것이 있다면
내부에서도 move를 통해, 그 포인터 주소를 넘겨받아야 한다

추가로 ‘상속’과 연관된 경우

class Base {
public:
    std::string base_str;

    Base(Base&& other) noexcept : base_str(std::move(other.base_str)) {
        std::cout << "Base Move\n";
    }
};

class Derived : public Base {
public:
    std::string derived_str;

    Derived(Derived&& other) noexcept
        : Base(std::move(other)),  // 명시적으로 부모 이동 생성자 호출
          derived_str(std::move(other.derived_str)) {
        std::cout << "Derived Move\n";
    }
};

부모 클래스의 이동 생성자/ 대입 연산자를 ‘자식’에서 호출해 주어야 한다

Perfect Forwarding

템플릿 함수가 전달받은 인자의 “값 특성”을 그대로 유지해
다른 함수로 전달하는 방식

필요한 이유?

void process(std::string& s)   { std::cout << "Lvalue\n"; }
void process(std::string&& s)  { std::cout << "Rvalue\n"; }

template <typename T>
void wrapper(T arg) {
    process(arg);  // 항상 lvalue로 처리됨!
}

std::string s = "hello";
wrapper(s);              // 기대: Lvalue → 결과: Lvalue ✅
wrapper(std::move(s));   // 기대: Rvalue → 결과: ❌ Lvalue

따라서

template <typename T>
void wrapper(T&& arg) {
    process(std::forward<T>(arg)); // ⭕ Forwarding
}

wrapper 함수를 std::forward(arg)를 사용함으로서
값의 속성을 보존한 상태로 전달 가능

std::forward

주어진 타입에 따라 일관되게 캐스팅을 해주는 함수
lvalue는 lvalue로, rvalue 는 rvalue(xvalue)로
그대로 캐스팅해주며
보통 다른 함수에게 넘기기 위한 용도로 사용

  • 템플릿 래퍼 함수
  • STL 함수 등에서 인자 최적화를 위해
  • emplace_back() 같이 내부에서 값을 생성하는 경우,
    각자의 생성자 와 이동 생성자를 분류할 수 있도록

댓글남기기