左值和右值
能取地址、有名字的是左值。不能取地址、没有名字的是右值。
比如对于&(b + c);这样的表达式就不能通过编译,因为b+c是右值,不能取地址。
C++11中又将右值进一步区分为纯右值和将亡值。
- 纯右值:临时变量(比如
a = b + c中的b + c)和字面值(比如a = 2中的2) - 将亡值: 1)返回右值引用的函数的调用表达式; 2)转换为右值引用的转换函数的调用表达式
右值引用是左值还是右值?
有名字的右值引用是左值,没有名字的右值引用是右值。
// 形参是个右值引用
void change(int&& right_value) {
right_value = 8;
}
int main() {
int a = 5; // a是个左值
int &ref_a_left = a; // ref_a_left是个左值引用
int &&ref_a_right = std::move(a); // ref_a_right是个右值引用
change(a); // 编译不过,a是左值,change参数要求右值
change(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值
change(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值
change(std::move(a)); // 编译通过
change(std::move(ref_a_right)); // 编译通过
change(std::move(ref_a_left)); // 编译通过
change(5); // 当然可以直接接右值,编译通过
cout << &a << ' ';
cout << &ref_a_left << ' ';
cout << &ref_a_right;
// 打印这三个左值的地址,都是一样的
}
// 以上代码来源于 https://zhuanlan.zhihu.com/p/335994370
或者说,作为函数返回值的 && 是右值,直接声明出来的 && 是左值
std::move的实现
std::move的原型是:
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
首先要关注的是,move的形参T会被编译器当做什么类型。还要先关注一个概念,引用折叠
引用折叠:
X& &、X&& &、X& &&都折叠成X&,用于处理左值。左值引用的左值引用、右值引用的左值引用、左值引用的右值引用都是左值引用。总结起来就是,只要有左值引用就会被折叠成左值引用。X&& &&折叠成X&&,用于处理右值。也就是说,右值引用的右值引用是右值引用。
在了解了引用折叠的概念之后,就能理解T会被推断为什么类型了。首先有一点要保证的是,T&&一定被推断成某种引用。
- 如果传入的实参是左值,那么
T&&应该是一个左值引用。根据引用折叠的规则,只有T是一个左值引用时,T&&才会是一个左值引用。 - 如果传入的实参是右值,那么
T&&应该是一个右值引用。那么,T应该被推断为非引用类型。
然后就是调用模板类remove_reference<T>了。这个类是通过模板泛化和特化实现的:
template <typename T> struct remove_reference{
typedef T type; //定义T的类型别名为type
};
//部分版本特例化,将用于左值引用和右值引用
template <class T> struct remove_reference<T&> //左值引用
{ typedef T type; }
template <class T> struct remove_reference<T&&> //右值引用
{ typedef T type; }
//举例如下,下列定义的a、b、c三个变量都是int类型
int i;
remove_refrence<decltype(42)>::type a; //使用原版本,
remove_refrence<decltype(i)>::type b; //左值引用特例版本
remove_refrence<decltype(std::move(i))>::type b; //右值引用特例版本
于是remove_reference这个类的作用是,如果传入的是T类型的左值引用或者右值引用,那么type成员可以萃取出T的类型(两个偏特化版本);如果传入的不是引用,也返回T的类型(泛化版本)。
再看下面的函数:
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
先推断出T的类型,然后通过remove_reference<T>::type萃取出非引用的类型,使用static_cast将其转化为右值引用类型。
最终,std::move实现了把左值、右值、左值引用、右值引用都转化为右值引用的功能。
std::forward的作用
第一个问题是,std::forward是为了解决什么问题的?
std::forward主要是配合万能引用的:
#include <iostream>
#include <stdexcept>
using namespace std;
class A {
public:
A(int&& n) {
cout << "A : rvalue chosen!" << endl;
}
A(int& n) {
cout << "A : lvalue chosen!" << endl;
}
};
template<class T>
void print(T&& t) {
A a(t);
}
int main()
{
int n = 2;
print(n);
print(2);
return 0;
}
print函数的参数T&&就是万能引用,不管实参是左值、右值、左值引用还是右值引用,它都能匹配。上面的函数运行的结果是:
A : lvalue chosen!
A : lvalue chosen!
第二次参数明明是一个右值,但是在print函数中,右值引用t本身是一个左值,于是传递给A的构造函数,调用了左值引用的版本。
而std::forward就是用来解决这种情况:如果我们想要调用A的右值引用版本的构造函数,也就是说保持t的右值引用的特性。这时候就需要使用std::forward:
A a(std::forward<T>(t));
std::forward的实现
template<typename _Tp>
inline _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t)
{ return static_cast<_Tp&&>(__t); }
template<typename _Tp>
inline _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t)
{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}
std::forward虽然是一个函数模板,但是它在使用的时候需要显式指定模板参数。就像上面的A a(std::forward<T>(t));。如果T的类型是右值引用,那么会匹配到第二个版本。因为remove_reference::value能萃取出_Tp本身的类型,于是第一个版本的参数是左值引用类型,第二个版本的参数是右值引用类型。
在匹配到第二个版本后,static_cast将__t转换为_Tp&&类型。由引用折叠可知,右值引用的右值引用仍然是右值引用,于是就返回了一个右值引用。这个右值引用是没有名字的,于是它本身是右值。
1864

被折叠的 条评论
为什么被折叠?



