Notice
Recent Posts
Recent Comments
Link
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

s-nova 님의 블로그

[C++] lvalue, rvalue, Perfect forwarding 본문

C++

[C++] lvalue, rvalue, Perfect forwarding

s-nova 2025. 9. 14. 22:56

C++에서 성능 최적화를 이야기할 때 빠질 수 없는 개념이 바로 lvalue / rvalue이동 의미론$^{\text{Move Semantics}}$입니다. 불필요한 복사를 줄이고 자원의 소유권을 효율적으로 전달하는 원리를 차근차근 살펴보겠습니다.

 

 

1. lvalue와 rvalue란?

C++의 모든 표현식은 lvalue$^{\text{left value}}$rvalue$^{\text{right value}}$ 중 하나로 분류됩니다.

 

구분 lvalue rvalue
정의 이름이 있는 변수 임시값, 리터럴, 계산 결과
주소 취득 가능 $($&x$)$ 불가능 $($&10 에러$)$
대입 연산자 왼쪽에 올 수 있음 오른쪽에만 올 수 있음
수명 선언된 스코프까지 유지 표현식이 끝나면 소멸

 

int a = 5;        // a: lvalue,  5: rvalue
int b = a;        // b: lvalue,  a: lvalue (이름이 있으므로)
int c = a + b;    // c: lvalue,  (a+b): rvalue (임시 계산 결과)

&a;               // OK   - lvalue는 주소 취득 가능
// &5;            // 에러 - rvalue는 주소 취득 불가
// &(a+b);        // 에러 - rvalue는 주소 취득 불가

a = 10;           // OK   - lvalue는 대입 가능
// 5 = 10;        // 에러 - rvalue는 대입 불가

 

 

2. 복사 생성자 vs 이동 생성자

lvalue와 rvalue를 구별하는 실질적인 이유는 바로 여기에 있습니다.

 

  • 복사$^{\text{Copy}}$: 새로운 메모리를 할당하고 데이터를 전부 복제합니다. 느리고 비용이 큽니다.
  • 이동$^{\text{Move}}$: 원본의 포인터를 그대로 가져오고 원본은 비웁니다. 포인터 교환만 일어나므로 빠릅니다.

 

C++은 인자가 lvalue이면 복사 생성자를, rvalue이면 이동 생성자를 자동으로 선택합니다. 이동 생성자는 && $($rvalue 참조$^{\text{rvalue reference}}$$)$로 선언합니다.

 

class BigData
{
    int* data;
    int size;
public:
    // 복사 생성자 (lvalue용) - 데이터 전체를 새로 할당
    BigData(const BigData& other)
    {
        size = other.size;
        data = new int[size];
        for (int i = 0; i < size; i++)
            data[i] = other.data[i];   // 느림
    }

    // 이동 생성자 (rvalue용) - 포인터만 가져옴
    BigData(BigData&& other)
    {
        data = other.data;             // 소유권 이전
        size = other.size;
        other.data = nullptr;          // 원본은 비워서 이중 해제 방지
    }
};

 

BigData createData() { return BigData(); }

// rvalue → 이동 생성자 선택 (빠름)
BigData d1 = createData();

// lvalue → 복사 생성자 선택 (느림)
BigData temp = createData();
BigData d2 = temp;

 

💡 std::move: lvalue를 강제로 rvalue로 캐스팅하고 싶을 때는 std::move()를 사용합니다. 단, 이후 원본 객체는 유효하지 않은 상태가 되므로 사용에 주의가 필요합니다.

 

 

3. 래퍼 함수의 문제 - 속성 손실

함수에 인자를 전달하는 순간 문제가 발생합니다. 인자가 함수의 매개변수 이름에 바인딩되는 순간, 그 인자는 이름이 생기므로 무조건 lvalue가 됩니다.

 

template
void wrap(T t)
{
    use(t);        // t는 이름이 있으므로 lvalue → 항상 복사 생성자 선택 (느림)
}

wrap(std::string("tmp"));  // 원래는 rvalue였지만, wrap 안에서 lvalue가 됨

 

rvalue를 넘겨줘도 래퍼 함수 안에서 lvalue로 바뀌어 버리므로, 이동이 아닌 복사가 호출됩니다. 래퍼 함수가 깊어질수록 이 문제는 더욱 심각해집니다.

 

 

4. Perfect Forwarding - 속성 보존 전달

이 문제를 해결하는 것이 Perfect Forwarding$^{\text{완벽한 전달}}$입니다. T&& $($forwarding reference$^{\text{전달 참조}}$$)$와 std::forward<T>()를 함께 사용하면 원래 인자가 lvalue였으면 lvalue로, rvalue였으면 rvalue로 그대로 전달됩니다.

 

📌 참고: 일반 함수에서 쓰이는 T&&는 rvalue 참조이지만, 템플릿에서 쓰이는 T&&는 forwarding reference가 됩니다. 두 가지는 문법이 같지만 의미가 다릅니다.

 

template
void wrap(T&& x)                     // forwarding reference
{
    callee(std::forward(x));      // lvalue면 lvalue로, rvalue면 rvalue로 전달
}

wrap(std::string("tmp"));            // rvalue → callee에 rvalue로 전달 (이동 선택)

 

게임 엔진의 팩토리 함수처럼 가변 인자$^{\text{variadic arguments}}$가 필요한 경우에도 동일하게 적용할 수 있습니다.

 

template
T* Spawn(Args&&... args)
{
    return new T(std::forward(args)...);  // 각 인자의 lvalue/rvalue 속성 그대로 전달
}

Spawn(100, 200, std::string("name"));  // rvalue → 이동 생성자 선택

std::string n = "boss";
Spawn(100, 200, n);                    // lvalue → 복사 생성자 선택

 

💡 게임 개발 관점: 게임 엔진의 오브젝트 생성 함수 $($Spawn, CreateObject 등$)$에 Perfect Forwarding을 적용하면, 호출부에서 전달한 인자의 성격을 그대로 내부 생성자까지 전달할 수 있어 불필요한 복사를 완전히 제거할 수 있습니다.

 

 

5. 핵심 정리

개념 문법 역할
rvalue 참조 T&& $($일반 함수$)$ 임시값을 바인딩, 이동 생성자/이동 대입 정의에 사용
forwarding reference T&& $($템플릿$)$ lvalue/rvalue 모두 받아 속성 보존
std::move std::move(x) lvalue를 rvalue로 캐스팅 $($소유권 포기$)$
std::forward std::forward<T>(x) 원래 속성 $($lvalue/rvalue$)$을 유지하여 전달

 

'C++' 카테고리의 다른 글

[C++] C++ 캐스팅  (0) 2025.09.28
[C++] 스레드, CPU 캐시  (0) 2025.09.18
[C++] 스마트 포인터  (0) 2025.09.08
[C++] 얕은 복사 vs 깊은 복사  (0) 2025.08.31
[C++] 다형성, virtual, override  (0) 2025.07.27