左值和右值
了解完美转发前,必须先了解左值和右值的概念,以及左值引用和右值引用的概念。参见:左值/右值引用和std::move
万能引用
万能引用是一种特殊的引用,它只能出现在模板函数和模板类中。并且,万能引用的格式固定,为T&& t,其中变量t就是万能引用,而T就是模板参数。例如下面的的代码:
1 2 3 4
| template <typename T> void func(T &&t) { }
|
引用折叠
上面说了万能引用的格式,那为什么万能引用是万能的?这就是引用折叠的用处了。
已知万能引用的模板参数T是可以推导为具体类型的,也就是说:T可以是string,也可以是string&,也可以是string&&等等。那么,展开后,T&& t不久变成了:string&& && t,这么多&,这是什么玩意儿?C++11立了规矩,太多&要折叠一下,于是便产生了引用折叠。
引用折叠的具体规则:
Type& &,Type&& &,Type& &&都折叠成Type&.Type&& &&折叠成Type&&.
那要怎么判断模板参数T最后被推断为什么类型呢?看下面代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| #include <iostream> #include <type_traits> #include <string> using namespace std;
template <typename T> void func(T &¶m) { if (std::is_same<string, T>::value) { std::cout << "string" << std::endl; } else if (std::is_same<string &, T>::value) { std::cout << "string&" << std::endl; } else if (std::is_same<string &&, T>::value) { std::cout << "string&&" << std::endl; } else if (std::is_same<int, T>::value) { std::cout << "int" << std::endl; } else if (std::is_same<int &, T>::value) { std::cout << "int&" << std::endl; } else if (std::is_same<int &&, T>::value) { std::cout << "int&&" << std::endl; } else { std::cout << "unkown" << std::endl; } }
int getInt() { return 10; }
int main() { int x = 1; func(1); func(x); func(getInt());
return 0; }
|
上述代码输出:
std::forward
实现完美转发的关键是std::forward,其定义如下:
1 2 3 4 5 6 7 8 9 10 11 12
| template <typename _Tp> [[__nodiscard__, __gnu__::__always_inline__]] constexpr _Tp &&forward(typename std::remove_reference<_Tp>::type &__t) noexcept { return static_cast<_Tp &&>(__t); }
template <typename _Tp> [[__nodiscard__, __gnu__::__always_inline__]] constexpr _Tp &&forward(typename std::remove_reference<_Tp>::type &&__t) noexcept { static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument" " substituting _Tp is an lvalue reference type"); return static_cast<_Tp &&>(__t); }
|
先看形参:
- 第一个函数的形参类型是
typename std::remove_reference<_Tp>::type &__t,前面std::remove_reference<_Tp>::type不理解,先不管。主要看后面& __t,这说明__t肯定是一个左值引用,左值引用当然要接收左值。类似于int& b = a,b就是一个左值引用,接收左值a。 - 第二个函数的形参也类似,
__t肯定是一个右值引用,右值引用当然要接收右值。
再看返回值:
两个函数的返回值都是static_cast<_Tp &&>(__t);,很显然,这是将__t的类型转换为_Tp &&。但_Tp &&到底是什么类型?这就要看_Tp的类型了。而_Tp又是我们在调用std::forward时指定的。以int为例:
1 2 3 4 5
| std::forward<int>(100); std::forward<int&>(100);
int x = 100 std::forward<int&&>(x);
|
下面是测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| #include <iostream> #include <type_traits> #include <string> #include <memory> using namespace std;
template <typename T> std::string type_name() { #if defined(__clang__) std::string pretty_function = __PRETTY_FUNCTION__; size_t start = pretty_function.find("T = ") + 4; size_t end = pretty_function.find("]", start); return pretty_function.substr(start, end - start);
#elif defined(__GNUC__) std::string pretty_function = __PRETTY_FUNCTION__; size_t start = pretty_function.find("T = ") + 4; size_t end = pretty_function.find(";", start); return pretty_function.substr(start, end - start);
#elif defined(_MSC_VER) std::string pretty_function = __FUNCSIG__; size_t start = pretty_function.find("type_name<") + 10; size_t end = pretty_function.find(">(void)"); return pretty_function.substr(start, end - start);
#else #error "Unsupported compiler" #endif }
template <typename T> void verify_forward_type(const char *description) { if constexpr (std::is_lvalue_reference_v<T>) { std::cout << description << " is an lvalue reference (" << (std::is_const_v<std::remove_reference_t<T>> ? "const " : "") << "T&)" << std::endl; } else if constexpr (std::is_rvalue_reference_v<T>) { std::cout << description << " is an rvalue reference (" << (std::is_const_v<std::remove_reference_t<T>> ? "const " : "") << "T&&)" << std::endl; } else { std::cout << description << " is not a reference (T)" << std::endl; } }
int &&getInt() { return std::move(10); }
int main() { int x = 10;
std::cout << type_name<decltype(std::forward<int>(100))>() << std::endl;
std::cout << type_name<decltype(std::forward<int &>(x))>() << std::endl;
std::cout << type_name<decltype(std::forward<int &&>(x))>() << std::endl;
std::cout << type_name<decltype(std::forward<const int &&>(100))>() << std::endl;
std::cout << type_name<decltype(getInt())>() << std::endl;
verify_forward_type<decltype(std::forward<const int &&>(x))>("std::forward<const int&&>(x)");
return 0; }
|
代码输出:
1 2 3 4 5 6
| int&& int& int&& const int&& int&& std::forward<const int&&>(x) is an rvalue reference (const T&&)
|
完美转发
先看一段程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| template<typename T> void print(T & t){ std::cout << "Lvalue ref" << std::endl; }
template<typename T> void print(T && t){ std::cout << "Rvalue ref" << std::endl; }
template<typename T> void testForward(T && v){ print(v); print(std::forward<T>(v)); print(std::move(v));
std::cout << "======================" << std::endl; }
int main(int argc, char * argv[]) { int x = 1; testForward(x); testForward(std::move(x)); }
|
这段程序的输出如下:
1 2 3 4 5 6 7 8
| Lvalue ref Lvalue ref Rvalue ref ====================== Lvalue ref Rvalue ref Rvalue ref ======================
|
对比前后两次输出,第一行和第三行都是一样的。只有第二行不一样。为什么?
因为在函数void testForward(T && v)中,无论调用testForward()时传入的是左值还是右值,也即:无论T&& v是左值引用还是右值引用,v都必定是右值。(左值引用和右值引用都属于左值),这在std::move中介绍过。
所以print(v)一定调用左值版本(因为v是左值),print(std::move(v))一定调用右值版本(因为std::move()返回右值)。
而std::forward<T>(v)不一样,它根据调用时传入的模板参数T的类型,决定最后的返回类型到底是左值引用还是右值引用,又因为左值引用一定是左值,而右值引用在作为返回值时是右值,所以也间接决定了返回类型到底属于左值还是右值。也就是说:通过模板参数T控制返回值是左值还是右值。
完美转发的应用场景
暂时不讨论。网上说的比较多的是工厂函数和包装器。但是个人用到的并不多。后面用到再补充。