1、万能引用与引用折叠
1.1、普通引用
-
之前的学习中学习了左值、右值、左值引用、右值引用、常引用等,但是很可惜它们都必须搭配固定的类型导致它们受到一些限制
void test1() {int a = 1, b = 2;int& left_ref_var = a; // int&& right_ref_var = a; // 报错, 右值引用只能引用右值// int& left_ref_val = 1; // 报错, 左值引用只能引用左值int&& right_ref_val = 1; int& left_ref_right = right_ref_val; // 左值引用 引用 右值引用(右值引用本质也是左值)const int& const_left_ref_val = 1; // 常量左值引用, 可以引用右值const int& const_left_ref_var = a; // 常量左值引用, 也可以引用左值 // const_left_ref_val = 2; // 报错 // const_left_ref_var = b; // 报错const int* const const_ptr_const = &a; // 常量左值引用的本质 }
- 上面这些例子很清楚的解释了上面的这些关键字的意思。
- 左值引用和右值引用都不够万能,而常量左值引用呢又不能修改,这就很头疼。
- 首先需要明确的一点:右值引用的虽然只能引用右值,但是它也是一个左值,因为它有地址所以是一个左值,因此左值引用可以引用右值引用(左值)
1.2、万能引用
-
而常量引用又不能修改,C++11中就开始提供一个万能引用的东西,跟&&一样,但是需要看它使用在什么地方。
void foo(int&& i) { } // i为右值引用, 是一个左值 template<class T> void bar(T&& t){ } // t是一个万能引用 int get_val(){ return 5;}void test2() {int&& x = get_val(); // x为右值引用auto&& y1 = 1; // y为万能引用int a = 5;auto&& y2 = a; std::cout << "y2 = " << y2 << ", &y2 = " << &y2 << std::endl;std::cout << " a = " << a << ", &a = " << &a << std::endl;a = 10;std::cout << "y2 = " << y2 << ", &y2 = " << &y2 << std::endl;std::cout << " a = " << a << ", &a = " << &a << std::endl;y2 = 123;std::cout << "y2 = " << y2 << ", &y2 = " << &y2 << std::endl;std::cout << " a = " << a << ", &a = " << &a << std::endl; }
万能引用:无论传入什么值还是引用都能够接收,并且确定其一个准确的类型,通过引用折叠的方式。
- 初始化的源对象如果是一个左值,则目标对象会推导出左值引用。
- 初始化的源对象如果是一个右值,则目标对象会推导出右值引用。
1.3、引用折叠
由于出现了万能引用,现在传入参数就会变得很复杂,一会传入左值一会传入右值,通过模版参数接手的时候,实际上编译器呢会进行引用折叠的一个操作
模板类型T | 实际类型R | 最终推导类型 |
---|---|---|
T& | R | R& |
T& | R& | R& |
T& | R&& | R& |
T&& | R | R&& |
T&& | R& | R& |
T&& | R&& | R&& |
- 上面的表格中显示了引用折叠的推导规则,可以看出在整个推导过程中只要有左值引用参与进来,最后推倒的结果就是一个左值引用。
- 其实最难理解的是第三条:模板是T的左值引用,遇到R&&右值引用,推导的结果是一个左值引用。其原因是因为右值引用的本质也是一个左值,等价于了第一条…
- 如果模板是一个右值引用,那么凑满3个&减去2个&就是最终的类型。
2、完美转发
C++11中完美转发std::forward在万能引用的基础在进行优化,对于一个对象如果传入什么类型就按照什么类型进行转发。
举个例子:小明有一本书,但是小明不看,我特别想看,那么我有两种获取的办法:
-
拷贝构造:我去找小明把书接过来复印一份,然后再阅读(这就叫拷贝构造)
-
移动构造:我直接找小明把书拿过来,反正小明也不看。(这就叫移动构造)
-
但是现在我不确定小明是借给我复印(拷贝)还是直接借给我看(移动),这需要去问小明,然后小明怎么说我就这么做!
-
完美转发:我按照小明给我的方式(拷贝or移动)使用,这就叫完美转发!
2.1、完美转发的引入
-
给定下面一个这样的例子:
-
一个左值引用的func_push、一个右值引用的func_push函数,二者是重载的关系
-
然后定义一个模板函数func(T&& t),给定万能引用T&& t类型,内部调用func_push函数
-
最后在外面测试传入效果
std::vector<std::string> v; void func_push(const std::string& str) {std::cout << "(const std::string& str)" << std::endl;v.push_back(str);} void func_push(std::string&& str) {std::cout << "(std::string&& str)" << std::endl;v.push_back(std::move(str)); } template<class T> void func(T&& t) {func_push(t); }void test3() {std::string s = "123";func(s);func(std::move(s)); //std::move(s)是一个亡值表达式, 返回的是一个右值传入给func函数 } /* 输出 (const std::string& str) (const std::string& str) */
-
-
首先分析一下输出结果:
- func(s):s是一个左值字符串,对于万能引用来说&& + & = &,因此引用折叠推导出是一个左值引用,调用左值引用的func_push
- func(std::move(s)):而这个东西就很有意思了,它也调用左值引用。
- std::move(s):这是一个亡值表达式,意思是移交s的所有权给func(T&& t),也就是说它是右值
- 此时对于万能引用来说&& + && ==> &&,引用折叠后推导出是一个右值引用,T&& t = std::move(s)。
- 而根据上面结论:虽然t现在是一个右值引用,但是t也是一个左值的本质!因此还是会调用左值引用的func_push
-
那现在的结果就跟我们的需求背道而驰了啊,希望func(std::move(s))调用右值引用的func_push
-
而如果把func中的func_push(t);改为 ==> func_push(std::move(t));,那么结果又都调用了右值引用的func_push
template<class T> void func(T&& t) { // func_push(t); // func_push(std::move(t)); }void test3() {std::string s = "123";func(s);func(std::move(s)); } /* 输出 (std::string&& str) (std::string&& str) */
这两份代码无论如何改,再不引入完美转发和类型转换之后是无法满足需求的。
2.2、完美转发
template<class T>
void func1(T&& t)
{
// func_push(static_cast<T&&>(t));func_push(std::forward<T>(t));
}
void test4()
{std::string s = "123";func1(s); // 输出: (const std::string& str)func1(std::move(s)); // 输出: (std::string&& str)std::string t;func1(std::move(t)); // 输出: (std::string&& str)
}
-
forward:按照给定的参数的具体类型进行原样转发,不修改其左右值属性。
-
输出结果分析:
func1(s);
:s是一个左值,传给函数模板func1匹配的进行引用折叠,得到一个左值引用,左值引用本身也是一个左值,完美转发时按照左值进行转发。func1(std::move(s))
:这里使用了移动语义,std::move(s)是一个亡值表达式,传入右值,经过引用折叠之后得到一个右值引用。此时重头戏来了:因为上面提到右值引用其实也是一个左值,但是使用完美转发后右值引用会按照右值转发!func1(std::move(t))
:同上
-
原理解析:
std::forward完美转发的源码就下面这些
template<typename _Tp> constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type& __t) noexcept { return static_cast<_Tp&&>(__t); } template<typename _Tp> 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); } template<typename _Tp> struct remove_reference { typedef _Tp type; };template<typename _Tp> struct remove_reference<_Tp&> { typedef _Tp type; };template<typename _Tp> struct remove_reference<_Tp&&> { typedef _Tp type; };
- 首先会调用std::remove_reference进行引用移除,如论是T、T&、T&&类型都最后返回T类型
- 然后会匹配到对应的不同forward的模板函数进行引用折叠,引用折叠完毕之后右值引用就是返回右值、左值引用就是返回左值。
- 其实我们可以看到核心就是static<T &&> (__t),我们也可以通过手动类型转化进行"完美转发"…