左值、右值和左值引用,在C++11之前,我们都很熟悉也都很好理解。左值(LValue)就是有名字能够寻址的对象的值类型,右值就是在内存上没有名字的数值的值类型,左值引用就是指向左值的引用。
C++11引入了右值引用,从而可以去读写在内存上临时的右值(C++11之前可以使用const左值引用去指向右值来读右值的内容,但是无法修改所指向的右值内容)。
C++11之前,任何对象不是左值就是右值,那引入右值引用之后,右值引用到底是左值还是右值呢?考虑到右值引用在行为使用上与右值有相似的地方,但也有冲突的地方,具体细节可以参考C++标准文档(n3055)。于是C++标准委员会将原来非黑即白的左值和右值两种类型细分成了5种类别,其中一些类别还会有共同重叠的部分。这5种类别中最值的关注的就是新创建了XValue,C++标准规定将一个右值引用绑定到一个对象,这个行为或者说是这个表达式的结果就是XValue。可见,XValue的诞生就是为右值引用量身定制的。那XValue这个名字起源于eXpiring这个单词,其实也很好理解,当我们使用一个右值引用想要move一个临时对象的值时,这个临时对象的生命周期也就即将结束。
除了XValue,另外4中类型分别是LValue, GLValue,RValue和PRValue。LValue还是传统意义上的左值。GLValue是generalized LValue,它包括LValue和XValue。PRValue是pure RValue,虽然是新的类型,但它就是之前传统意义上的右值。RValue现在是一个统称,它包括PRValue和XValue。具体包含关系可以见下图:
需要注意的是,XValue它既是GLValue,也是RValue,所以这里是与的关系。
从图中可以看出,C++11之后,任何一个值,它肯定是LValue,XValue和PRValue中的一种。LValue和PRValue,我们都很好理解,那XValue到底会在哪些情况下出现用到呢?
C++标准规定了以下这些情况下的表达式的结果都是XValue:
- 无论是显式还是隐式调用一个函数,只要函数返回值的类型是指向对象的右值引用,那么调用的结果就是XValue;
- 将现有的对象转换成指向对象的右值引用,这个行为或者叫表达式的结果就是XValue;
- 如果一个对象本身已经是XValue,那访问这个对象的非static的成员变量的表达式的结果就是XValue;
- 如果一个对象本身已经是XValue,并且通过.*pointer-to-member的方式来访问成员变量,那这个表达式的结果就是XValue
总而言之,以上这些规则表明,有名字的(named)右值引用将会被视为左值,没名字的(unnamed)右值引用将会被视为XValue。下面是这些规则的一个例子:
struct A {
int m;
};
A&& operator+(A, A);
A&& f();
A a;
A&& ar = static_cast<A&&>(a);
表达式f(),表达式f().m,表达式static_cast<A&&>(a),表达式 a + a,以上这些表达式的结果都是XValue。表达式ar是一个左值,它的类型是A。根据文档中的解释,如果一个表达式是一个指向类型T的引用,那么这个表达式的类型将会调整为类型T。
上面的例子中static_cast<A&&>(a)是XValue,而std::move的实现正是使用了static_cast<X&&>:
template<typename T> struct remove_reference { typedef T type; };
template<typename T> struct remove_reference<T&> { typedef T type; };
template<typename T> struct remove_reference<T&&> { typedef T type; };
template<typename T>
constexpr typename remove_reference<T>::type && move(T && arg) noexcept
{
return static_cast<typename remove_reference<T>::type &&>(arg);
}
所以,std::move的返回值是一个XValue,而XValue也是一个右值,所以std::move的返回值也是一个右值,这也是为什么使用std::move可以触发调用move constructor/assignment,而不是调用copy constructor/assignment:
struct X
{
std::string s_;
X(){}
X(const X & other) : s_{ other.s_ } {}
X(X && other) noexcept : s_{ std::move(other.s_) } {}
// other is an lvalue, and other.s_ is an lvalue too
// use std::move to force using the move constructor for s_
// don't use other.s_ after std::move (other than to destruct)
};
int main()
{
X a;
X b = std::move(a);
// a is an lvalue
// use std::move to convert to a rvalue,
// xvalue to be precise,
// so that the move constructor for X is used
// don't use a after std::move (other than to destruct)
}
参考资料:
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n3055.pdf