코딩하는 두식이

Lvalue Rvalue std::move 본문

공부/C++

Lvalue Rvalue std::move

털빙이 2022. 6. 28. 02:45

Lvalue와 Rvalue의 구분

 

C++에서 모든 표현식은 Lvalue 또는 Rvalue 입니다. Lvalue는 단일 표현식 이후에도 없어지지 않고 지속되는 객체입니다. 쉽게 생각해서 이름을 가지는 객체는 Lvalue라고 얘기할 수 있죠. 그러므로 const 타입을 포함한 모든 변수는 Lvalue 입니다. 반면에 Rvalue는 표현식이 종료된 이후에는 더이상 존재하지 않는 임시적인 값입니다. 상수 또는 임시 객체는 Rvalue 라고 얘기할 수 있겠네요. 그럼 이해를 돕기 위해서 예제를 통해 Lvalue와 Rvalue를 구분해 보도록 하겠습니다.

 

#include <iostream>
#include <string>
using namespace std;
int main()
{
    int x = 3;
    const int y = x;
    int z = x + y;
    int* p = &x;


    cout << string("one");

    ++x;
    x++;
}

 

  

Rvalue를 구분하기 쉽게 굵은 밑줄로 표시하였는데 조금 구분이 되시나요? x, y, z, p 등의 이름을 가지는 변수는 모두 Lvalue 이지만 상수값 3, 임시객체 string("one") 은 표현식이 종료되면 더이상 참조할 수 없는 값이기 때문에 Rvalue 입니다. x + y, &x 와 같은 표현식도 마찬가지이죠. 또 한가지 흥미로운 점은 ++x는 Lvalue인 반면에 x++은 RValue라는 점입니다. 둘 다 증가된 값을 리턴하지만 ++x는 증가된 x 자신을 리턴하기 때문에 Lvalue인 반면에 x++은 증가된 복사본을 리턴하기 때문에 Rvalue 이죠. 이렇게해도 Lvalue와 Rvalue가 잘 구분이 안되신다면 좀 더 확실하게 구분하는 방법이 있습니다. 바로 표현식에 주소 연산자 &를 붙여보는 건데요, & 연산자는 Lvalue를 요구하기 때문에 표현식이 Rvalue라면 컴파일 오류가 나게됩니다.

 

&(++x);
&(x++);     // error C2102: '&' requires l-value

 

 

Rvalue 참조자 &&

 

지금까지 C++에서 int& a = b; 형태로 사용하였던 참조자(Reference)는 Lvalue 참조자입니다. C++11 표준에서는 Lvalue 참조자 이외에도 Rvalue를 참조할 수 있는 Rvalue 참조자가 추가되었습니다. 이름에서 어느정도 예상할 수 있듯이 Lvalue 참조자는 Lvalue만 참조할 수 있고 Rvalue 참조자는 Rvalue만 참조할 수 있습니다. Rvalue 참조자는 Visual Studio 2010 이상 버전의 컴파일러에서 사용 가능합니다.

 

int rvalue()
{
    return 10;
}

int main()
{
    int lvalue = 10;

    int& a = lvalue;
    int& b = rvalue();      // error C2440: 'initializing' : cannot convert from 'int' to 'int &'

    int&& c = lvalue;       // error C2440: 'initializing' : cannot convert from 'int' to 'int &&'
    int&& d = rvalue();
}

 

 

std::move 를 사용해서 성능을 개선해보자

std::move 는 이름 때문에 부르는 것과 동시에 어떤 이동 작업이 이뤄질 것 같지만, 실제로 Lvalue 를 Rvalue 로 casting 해줄 뿐이다.

 

(std::string 은 실제 작은 문자열은 stack memory 에 저장하고 큰 문자열은 heap memory 에 저장하지만, 설명에선 편의를 위해 항상 heap 에 저장한다고 가정한다.)

위 코드에서 B = A 수행 시 copy 가 일어난다. (copy assignment operator)
C = std::move(A) 수행 시 move 가 일어난다. (move assignment operator)

B = A 를 할 때는 copy 하기 때문에 B.data 는 당연히 “aaa” 가 복사되고 A.data 도 여전히 “aaa”를 가지고 있지만
C = std::move(A) 를 하면 move 하기 때문에 A.data 는 C.data 로 이동이 되어서 A.data 는 빈 문자열이 되고 C.data 는 “aaa” 를 가진다. 얻을 수 있는 이점은, 새로 메모리를 할당(malloc)하지 않아도 되고 이미 메모리에 할당된 것을 소유권만 C 에게 넘겨주기 때문에 copy 동작보다 빠르다. (move 로 할당한 이후로는 A를 사용하지 못하므로 주의)