본문 바로가기

카테고리 없음

C++ Rvalue Reference (2)

Universal References

T&& Doesn’t Always Mean “Rvalue Reference”  by Scott Meyers

 

보편 참조(Universal Reference)는 C++ 11에서 도입된 개념으로 특정 문맥에서 T&& 형태로 사용될 때 L-Value와 R-Value 모두 바인딩 될 수 있는 특별한 참조를 의미한다. 즉 T&&은 보편 참조로도 사용될 수 있으므로 항상 R-Value로 사용되는 것만은 아니라는 뜻이다. 개념만 읽어선 쉽게 와닿지 않을 수 있다.

 

L-Value Reference, R-Value Reference는 문법적으로 구분되어 있기 때문에 함수를 직접 부르는 경우는 문제가 없었다.

문제는 template과 같이 사용했을 때 발생한다. 

#include <bits/stdc++.h>

struct myStr {};
void func(myStr& str)
{
	std::cout << "func(): L-value reference\n";
}
void func(myStr&& str)
{
	std::cout << "func(): R-value reference\n";
}

template<typename T>
void func_wrapper(T&& t)
{
	if (std::is_lvalue_reference<decltype(t)>::value)
	{
		std::cout << "func_wrapper(): L-value reference\n";
	}
	if (std::is_rvalue_reference<decltype(t)>::value)
	{
		std::cout << "func_wrapper(): R-value reference\n";
	}
	func(t);
}

int main()
{
	myStr str;
	func(str); // func(): L-value reference
	func_wrapper(str); // func_wrapper(): L-value reference, func(): L-value reference


	func(myStr()); // func(): R-value reference
	func_wrapper(myStr()); // func_wrapper(): R-value reference, func(): L-value reference
	return 0; 
}

 

func_wrapper(myStr())에서 넘긴 R-Value 값이 유지되며 func()에서도 R-Value reference가 호출될 것이라는 예상과 다르게 template과 연계하여 사용하면 모두 L-Value Reference를 호출하는 것을 볼 수 있다. 

 

std::move()와 같이 사용하면 되는거 아닌가?

func_wrapper()에서 func(t)가 아닌 func(std::move(t))로 넘기면 된다고 생각할 수 있다.

그러나 move는 L-Value가 들어오든 R-Value가 들어오든 무조건 R-Value로 만들어버린다.

#include <bits/stdc++.h>

struct myStr {};
void func(myStr& str)
{
	std::cout << "func(): L-value reference\n";
}
void func(myStr&& str)
{
	std::cout << "func(): R-value reference\n";
}

template<typename T>
void func_wrapper(T&& t)
{
	if (std::is_lvalue_reference<decltype(t)>::value)
	{
		std::cout << "func_wrapper(): L-value reference\n";
	}
	if (std::is_rvalue_reference<decltype(t)>::value)
	{
		std::cout << "func_wrapper(): R-value reference\n";
	}
	func(std::move(t));
}

int main()
{
	myStr str;
	func_wrapper(str); // func_wrapper(): L-value reference, func(): R-value reference
	func_wrapper(myStr()); // func_wrapper(): R-value reference, func(): R-value reference
	return 0; 
}

 

move()를 쓰면 모두 R-Value로 처리되고, 안쓰면 모두 L-Value로 처리된다.

L-Value는 L-Value로, R-Value는 R-Value로 처리하도록 만들 수는 없는 것인가?

 

Perfect forwarding

한국말로 하면 완벽한 전달쯤으로 번역된다.

템플릿 함수가 인자를 받은 후, 그 인자를 다른 함수로 그대로 전달할 때 원래 인자의 값 범주(lvalue / rvalue)와

한정자(const / volatile)를 그대로 유지하여 전달하는 기법을 말한다. 함수를 래핑하는 상황에서 특히 중요하게 사용된다.

이러한 완벽한 전달을 만들어주는 함수가 이미 내장되어있다. std::forward()를 사용하면 해당 문제를 해결할 수 있다. 

여기서 템플릿 함수의 매개변수는 위에서 설명한 보편 참조(Universal Reference)와 같이 사용되어야 한다. 따라서 매개 변수 타입은 T&&으로 두고 같이 사용하면 된다.  

#include <bits/stdc++.h>

struct myStr {};
void func(myStr& str)
{
	std::cout << "func(): L-value reference\n";
}
void func(myStr&& str)
{
	std::cout << "func(): R-value reference\n";
}

template<typename T>
void func_wrapper(T&& t)
{
	if (std::is_lvalue_reference<decltype(t)>::value)
	{
		std::cout << "func_wrapper(): L-value reference\n";
	}
	if (std::is_rvalue_reference<decltype(t)>::value)
	{
		std::cout << "func_wrapper(): R-value reference\n";
	}
	func(std::forward<T>(t));
}

int main()
{
	myStr str;
	func_wrapper(str); // func_wrapper(): L-value reference, func(): L-value reference
	func_wrapper(myStr()); // func_wrapper(): R-value reference, func(): R-value reference
	return 0; 
}

 

Reference collapsing Rules

이렇게 L-Value, R-Value 개념이 장황해지는 이유는 특정 문맥에 따라 다르게 처리되기 때문이다. 당연히 C++ 내부에서는 이러한 문맥에 대하여 어떻게 처리될지 규칙이 정해져 있다.

Left Right Result
T& & T&
T& && T&
T&& & T&
T&& && T&&

 

복잡해 보이지만 L-Value Reference가 섞이면 L-Value Reference가 된다고 생각하면 편하다.

 

아래는 auto, typedef, using과 같이 사용된 예시이다.

auto

std::vector<int> vec_0{0, 1, 2};
auto&& vec_1{vec_0}; // auto&& -> std::vector<int>& && -> std::vector<int>&
auto&& vec_2{std::move(vec_0)}; // auto&& -> std::vector<int>&& && -> std::vector<int>&&

 

Type Alias

typedef int&  lref;
typedef int&& rref;
int n;
lref&  r1 = n; // type of r1 is int&
lref&& r2 = n; // type of r2 is int&
rref&  r3 = n; // type of r3 is int&
rref&& r4 = 1; // type of r4 is int&&

 

using lref = int&;
using rref = int&&;
int n;
lref&  r1 = n; // type of r1 is int&
lref&& r2 = n; // type of r2 is int&
rref&  r3 = n; // type of r3 is int&
rref&& r4 = 1; // type of r4 is int&&