Creative Motive

Rvalue Reference #3 - Rvalue Reference 속성 본문

C++

Rvalue Reference #3 - Rvalue Reference 속성

aicosmos 2013. 2. 22. 13:46

데브피아 김경진님 작성 (http://devmachine.blog.me/176806250)


Rvalue Reference 속성

함수 파라미터로 Lvalue 또는 Rvalue를 전달받고 이에 따른 처리가 달라지는 함수를 구현할 경우 각각 Lvalue reference와 Rvalue Reference를 파라미터로 사용하는 오버로드 함수를 작성할 수 있습니다.

아래 예제에서는 const lvalue reference와 rvalue reference를 각각 파라미터로 전달받는 오버로드 함수를 작성하고 Lvalue와 Rvalue를 이용해 오버로드 함수를 호출하였을 경우 어떤 함수가 호출되어지는지 보여줍니다.

#include <iostream>
using namespace std;

class MemoryBlock
{
// MemoryBlock 구현
};

void f(const MemoryBlock&)
{
cout << "In f(const MemoryBlock&). This version cannot modify the parameter." << endl;
}

void f(MemoryBlock&&)
{
cout << "In f(MemoryBlock&&). This version can modify the parameter." << endl;
}

int main()
{
MemoryBlock block;
f(block);
f(MemoryBlock());
}

In f(const MemoryBlock&). This version cannot modify the parameter.
In f(MemoryBlock&&). This version can modify the parameter.

이미 Lvalue와 Rvalue에 대하여 충분히 설명드렸기 때문에 쉽게 결과를 예상하셨으리라 생각합니다. 첫번째 f 호출시에는 Lvalue인 지역변수 block을 사용했기 때문에 void f(const MemoryBlock&) 함수가 호출되고 두번째 f 호출시에는 Rvalue인 임시객체를 사용했기 때문에 void f(MemoryBlock&&) 함수가 호출됩니다. 사실 Rvalue를 이용하여 f 함수를 호출하면 문법적으로는 두 가지 버전 모두 호출이 가능합니다만 T&&의 우선순위가 const T&의 우선순위보다 높기 때문에 위와 같은 결과가 나타납니다.

Lvalue를 Rvalue reference 타입으로 타입 캐스팅하여 사용 가능합니다.

std::move 함수를 사용하면 Lvalue를 Rvalue reference 타입으로 타입 캐스팅 할 수 있습니다. 그리고 이것은 static_cast를 이용하여 Lvalue를 Rvalue reference로 직접 타입 캐스팅하는 것과 같은 역할을 합니다.

#include <iostream>
using namespace std;

class MemoryBlock
{
// MemoryBlock 구현
};

void g(const MemoryBlock&)
{
cout << "In g(const MemoryBlock&)." << endl;
}

void g(MemoryBlock&&)
{
cout << "In g(MemoryBlock&&)." << endl;
}

int main()
{
MemoryBlock block;
g(block);
g(move(block));
g(static_cast<MemoryBlock&&>(block));
}

In g(const MemoryBlock&).
In g(MemoryBlock&&).
In g(MemoryBlock&&).

g 함수를 호출할 때 move 함수를 사용하여 전달할 경우와 static_cast로 타입 캐스팅 하여 전달할 경우 Rvalue reference를 파라미터로 전달받는 void g(MemoryBlock&&) 함수가 호출되는 것을 볼 수 있습니다.

변수로서 이름 붙여진 Rvalue reference는 그 이후부터 Rvalue가 아닌 Lvalue로 취급됩니다.

Rvalue reference를 파라미터로 전달받는 함수를 작성할 경우 그 파라미터는 함수 내부에서는 Lvalue로 취급됩니다. Rvalue는 프로그램 어디에서도 참조될 수 없는 임시 객체이지만 Rvalue reference를 통해 변수로서 이름 붙여지게 되면 여러곳에서 참조가 가능해지므로 더 이상 Rvalue가 아니게 되는거죠. 아래 예제의 결과는 이러한 특징을 잘 보여주고 있습니다.

#include <iostream>
using namespace std;

class MemoryBlock
{
// MemoryBlock 구현
};

void g(const MemoryBlock&)
{
cout << "In g(const MemoryBlock&)." << endl;
}

void g(MemoryBlock&&)
{
cout << "In g(MemoryBlock&&)." << endl;
}

MemoryBlock&& f(MemoryBlock&& block)
{
// block은 Rvalue Reference 이므로 함수 내에서 Lvalue로 취급
g(block);
// Lvalue를 Rvalue Reference로 타입 캐스팅하여 리턴
return move(block);
}

int main()
{
g(f(MemoryBlock()));
}

In g(const MemoryBlock&).
In g(MemoryBlock&&).

임시 객체를 생성하여 f 함수를 호출하면 내부에서 파라미터 block을 이용하여 g 함수를 다시 호출하지만 block은 이름 붙여진 Rvalue reference이기 때문에 Lvalue로 취급되어 void g(const MemoryBlock&) 함수가 호출됨을 볼 수 있습니다. 그리고 f 함수의 리턴값은 이름이 붙여지지 않은 Rvalue 이기 때문에 두번째 g 함수 호출은 void g(MemoryBlock&&) 함수가 호출됩니다.

Rvalue Reference 타입을 템플릿 파라미터로 가지는 템플릿 함수가 템플릿 파라미터의 타입을 추론할 때 Reference Collapsing Rules 규칙이 사용됩니다.

다음과 같은 템플릿 함수가 있다고 가정해봅시다.

template <typename T>
void deduce(T&& x);

템플릿 함수가 Lvalue에 의해 호출되면 T의 타입은 T& 로 추론되고 Rvalue에 의해 호출되면 T의 타입은 T로 추론됩니다. Lvalue와 Rvalue에 의해 호출된 템플릿 함수는 각각 다음과 같은 형식으로 추론되겠죠.

// Lvalue에 의한 호출
void deduce(T& && x);

// Rvalue에 의한 호출
void deduce(T&& x);

Rvalue에 의한 호출은 T&&로 타입이 명확해졌으나 Lvalue에 의한 호출 시에는 T& && 타입으로 판단하기 애매한 타입이 되어버렸습니다. 바로 이러한 경우 Reference Collapsing Rules 규칙이 적용됩니다.

Reference Collapsing Rules

  • T& & -> T&
  • T& && -> T&
  • T&& & -> T&
  • T&& && -> T&&

결국 deduce 템플릿 함수는 Reference Collapsing Rules에 의해 최종적으로 다음과 같은 형태로 변환됩니다.

// Lvalue에 의한 호출
void deduce(T& x);

// Rvalue에 의한 호출
void deduce(T&& x);

이해가 되셨나요? ^^ 이 규칙은 다음 강좌인 Perfect Forwarding 을 이해하기 위하여 반드시 필요한 부분이므로 잘 기억해놓으시기 바랍니다.

Reference

MSDN - Rvalue Reference Declarator: &&