C++

C++ Rvalue 와 std::move 에 대한 이해

Rvalue 란 무엇인가?

Rvalue, 우측값은 대입 시에 항상 오른쪽에만 오는 식을 말한다.
예제로 이해하는 것이 쉽다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int doSomething()
{
...
return z;
}

int x = 10; // x 는 Lvalue, 10은 Rvalue
int y = 20; // y 는 Lvalue, 20은 Rvalue

x = y; // x 와 y 모두 Lvalue
x = (x + y); // x 는 Lvalue, x + y 는 Rvalue

x + y = 30; // 잘 못된 코드, Rvalue 인 x + y 에 대입하고 있다.

x = doSomething() // doSomething() 은 Rvalue

x, y 처럼 이름을 가지고 있고, 그 이름으로 접근할 수 있는 것이 Lvalue 이다.
그와 반대로 (x + y) 나 상수 10, 20 등은 이름을 가지고 있지 않고 식이 끝난 후 다음 라인에서 그 값에 접근할 수 없다.

Rvalue 를 함수의 매개변수로 받고 싶으면 A&& 로 선언한다.
참고로 doSomethingWithRvalue 안에서 rhs 를 다시 사용할 때는 rhs 가 Lvalue 라는 것을 주의하자. Rvalue 로 사용하고 싶으면 다시 std::move(rhs) 와 같이 사용해주어야 한다.

1
2
3
4
5
6
class A;
// class A의 instance를 Rvalue 로 받고 싶을 때
void doSomethingWithRvalue(A&& rhs)
{

}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class myClass
{
public:
myClass()
{
}
myClass(std::string str)
: data(str)
{
}
std::string data;
}

myClass A("aaa");
myClass B;
myClass C;

B = A;
C = std::move(A); // Lvalue 인 A를 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를 사용하지 못하므로 주의)

예제를 하나 더 보자.

1
2
3
4
5
std::vector<std::string> stringList;
{
std::string input = "example";
stringList.push_back(input);
}

std::string 을 원소로 가지는 vector stringList 가 선언되어 있고, input 을 push_back 해준다.
push_back 이 후에 input은 다시 쓰이지 않기 때문에 위 코드에서 input 은 vector에 string 을 넣기위해서만 만들어진 임시객체의 역할 밖에 되지 않는다. 이럴 때 std::move 를 사용하면 성능 향상이 될 수 있다.

1
2
3
4
5
std::vector<std::string> stringList;
{
std::string input = "example";
stringList.push_back(std::move(input));
}

그리고 예제에선 std::string 이 move constructor / move assignment operator 를 지원하기 때문에 move로 기대하는 동작을 얻을 수 있는 것이고, 사용자가 정의한 class 의 경우 destructors 나 assignment operators 를 명시적으로 선언했다면 move constructor 나 move assignment operators 가 암시적으로 생성되지 않으므로 직접 작성해주거나 default 로 선언해줘야 할 것이다.

암시적으로 생성되는 move constructors 의 형태는 아래와 같다. 기본적으로 클래스 멤버변수를 std::move 해서 Rvalue 로 casting 해 초기화한다.

1
2
3
4
5
6
7
8
9
10
11
12
class myClass
{
public:
myClass(myClass&& rhs)
: A(std::move(rhs.A))
, B(std::move(rhs.B))
{
}

std::string A;
std::string B;
}