Creative Motive
Rvalue Reference #3 - Rvalue Reference 속성 본문
데브피아 김경진님 작성 (http://devmachine.blog.me/176806250)
Rvalue Reference 속성
함수 파라미터로 Lvalue 또는 Rvalue를 전달받고 이에 따른 처리가 달라지는 함수를 구현할 경우 각각 Lvalue reference와 Rvalue Reference를 파라미터로 사용하는 오버로드 함수를 작성할 수 있습니다.
아래 예제에서는 const lvalue reference와 rvalue reference를 각각 파라미터로 전달받는 오버로드 함수를 작성하고 Lvalue와 Rvalue를 이용해 오버로드 함수를 호출하였을 경우 어떤 함수가 호출되어지는지 보여줍니다.
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(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로 직접 타입 캐스팅하는 것과 같은 역할을 합니다.
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(MemoryBlock&&).
In g(MemoryBlock&&).
g 함수를 호출할 때 move 함수를 사용하여 전달할 경우와 static_cast로 타입 캐스팅 하여 전달할 경우 Rvalue reference를 파라미터로 전달받는 void g(MemoryBlock&&) 함수가 호출되는 것을 볼 수 있습니다.
변수로서 이름 붙여진 Rvalue reference는 그 이후부터 Rvalue가 아닌 Lvalue로 취급됩니다.
Rvalue reference를 파라미터로 전달받는 함수를 작성할 경우 그 파라미터는 함수 내부에서는 Lvalue로 취급됩니다. Rvalue는 프로그램 어디에서도 참조될 수 없는 임시 객체이지만 Rvalue reference를 통해 변수로서 이름 붙여지게 되면 여러곳에서 참조가 가능해지므로 더 이상 Rvalue가 아니게 되는거죠. 아래 예제의 결과는 이러한 특징을 잘 보여주고 있습니다.
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(MemoryBlock&&).
임시 객체를 생성하여 f 함수를 호출하면 내부에서 파라미터 block을 이용하여 g 함수를 다시 호출하지만 block은 이름 붙여진 Rvalue reference이기 때문에 Lvalue로 취급되어 void g(const MemoryBlock&) 함수가 호출됨을 볼 수 있습니다. 그리고 f 함수의 리턴값은 이름이 붙여지지 않은 Rvalue 이기 때문에 두번째 g 함수 호출은 void g(MemoryBlock&&) 함수가 호출됩니다.
Rvalue Reference 타입을 템플릿 파라미터로 가지는 템플릿 함수가 템플릿 파라미터의 타입을 추론할 때 Reference Collapsing Rules 규칙이 사용됩니다.
다음과 같은 템플릿 함수가 있다고 가정해봅시다.
void deduce(T&& x);
템플릿 함수가 Lvalue에 의해 호출되면 T의 타입은 T& 로 추론되고 Rvalue에 의해 호출되면 T의 타입은 T로 추론됩니다. Lvalue와 Rvalue에 의해 호출된 템플릿 함수는 각각 다음과 같은 형식으로 추론되겠죠.
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에 의해 최종적으로 다음과 같은 형태로 변환됩니다.
void deduce(T& x);
// Rvalue에 의한 호출
void deduce(T&& x);
이해가 되셨나요? ^^ 이 규칙은 다음 강좌인 Perfect Forwarding 을 이해하기 위하여 반드시 필요한 부분이므로 잘 기억해놓으시기 바랍니다.
Reference