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&&