左值和右值
能取地址、有名字的是左值。不能取地址、没有名字的是右值。
比如对于&(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;
int &ref_a_left = a;
int &&ref_a_right = std::move(a);
change(a);
change(ref_a_left);
change(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;
}
或者说,作为函数返回值的 && 是右值,直接声明出来的 && 是左值
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;
};
template <class T> struct remove_reference<T&>
{ typedef T type; }
template <class T> struct remove_reference<T&&>
{ typedef T type; }
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&& 类型。由引用折叠可知,右值引用的右值引用仍然是右值引用,于是就返回了一个右值引用。这个右值引用是没有名字的,于是它本身是右值。
|