C++的表达式要不然是右值(rvalue),要不然就是左值(lvalue)。这两个名词是从C语言继承过来的,原本是为了帮助记忆:左值可以位于赋值语句的左侧,右值则不能。
在C++语言中,二者的区别就没那么简单了。一个左值表达式的求值结果是一个对象或者一个函数,然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象。此外,虽然某些表达式的求值结果是对象,但它们是右值而非左值。可以做一个简单的归纳:当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。左值与右值的根本区别在于是否允许取地址&运算符获得对应的内存地址。
不同的运算符对运算对象的要求各不相同,有的需要左值运算对象,有的需要右值运算对象;返回值也有差异,有的得到左值结果,有的得到右值结果。一个重要的原则(有一种例外的情况),是在需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。当一个左值被当成右值使用时,实际使用的是它的内容(值)。
运算符用到左值的包括:(1).赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也仍然是一个左值。(2).取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。(3). 内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符的求值结果都是左值。(4). 内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本所得的结果也是左值。
使用关键字decltype的时候,左值和右值也有所不同。如果表达式的求值结果是左值,decltype作用于该表达式(不是变量)得到一个引用类型。举个例子,假定p的类型是int*,因为解引用运算符生成左值,所以decltype(*p)的结果是int&。另一方面,因为取地址运算符生成右值,所以decltype(&p)的结果是int**,也就是说,结果是一个指向整型指针的指针。
赋值运算符的左侧运算对象必须是一个可修改的左值。赋值运算符的结果是它的左侧运算对象,并且是一个左值。相应的,结果的类型就是左侧运算对象的类型。如果赋值运算符的左右两个运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型。
递增和递减运算符有两种形式:前置版本和后置版本。这两种运算符必须作用于左值运算对象。前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回。
箭头运算符作用于一个指针类型的运算对象,结果是一个左值。点运算符分成两种情况:如果成员所属的对象是左值,那么结果是左值;反之,如果成员所属的对象是右值,那么结果是右值。
当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果是左值;否则运算的结果是右值。
对于逗号运算符来说,首先对左侧的表达式求值,然后将求值结果丢弃掉。逗号运算符真正的结果是右侧表达式的值。如果右侧运算对象是左值,那么最终的求值结果也是左值。
左值(lvalue):是指那些求值结果为对象或函数的表达式。一个表示对象的非常量左值可以作为赋值运算符的左侧运算对象。
右值(rvalue):是指一种表达式,其结果是值而非值所在的位置。
函数返回类型:函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其它返回类型得到右值。可以像使用其它左值那样来使用返回引用的函数的调用,特别是,我们能为返回类型是非常量引用的函数的结果赋值。把函数调用放在赋值语句的左侧可能看起来有点奇怪,但其实这没什么特别的。返回值是引用,因此调用是个左值,和其它左值一样它也能出现在赋值运算符的左侧。如果返回类型是常量引用,我们不能给调用的结果赋值,这一点和我们熟悉的情况是一样的。
严格来说,当我们使用术语”引用(reference)”时,指的其实是”左值引用(lvalue reference)”。C++11中新增了一种引用,右值引用(rvalue reference),这种引用主要用于内置类。
为了支持移动操作,C++11引入了一种新的引用类型----右值引用(rvalue reference)。所谓右值引用就是必须绑定到右值的引用。通过&&而不是&来获得右值引用。右值引用有一个重要的性质----只能绑定到一个将要销毁的对象。因此,可以自由地将一个右值引用的资源”移动”到另一个对象中。
左值和右值是表达式的属性。一些表达式生成或要求左值,而另外一些则生成或要求右值。一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。
类似任何引用,一个右值引用也不过是某个对象的另一个名字而已。对于常规引用(为了与右值引用区分开来,可以称之为左值引用(lvalue reference)),不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的绑定特性:可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上。
返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子。可以将一个左值引用绑定到这类表达式的结果上。
返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。不能将一个左值引用绑定到这类表达式上,但可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。
左值持久,右值短暂:左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
由于右值引用只能绑定到临时对象,可知:所引用的对象将要被销毁;该对象没有其它用户。这两个特性意味着,使用右值引用的代码可以自由地接管所引用的对象的资源。右值引用指向将要销毁的对象。因此,我们可以从绑定到右值引用的对象”窃取”状态。
变量是左值:变量可以看作只有一个运算对象而没有运算符的表达式,虽然我们很少这样看待变量。类似其它任何表达式,变量表达式也有左值/右值属性。变量表达式都是左值。带来的结果就是,我们不能将一个右值引用绑定到一个右值引用类型的变量上。其实有了右值表示临时对象这一观察结果,变量是左值这一特性并不令人惊讶。毕竟,变量是持久的,直至离开作用域时才被销毁。变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。
标准库std::move函数:虽然不能将一个右值引用直接绑定到一个左值上,但可以显示地将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用,此函数定义在头文件utility中。move调用告诉编译器:有一个左值,但希望像一个右值一样处理它。我们必须认识到,在调用move之后,我们不能对移后源对象的值做任何假设。我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。
移动构造函数和移动赋值运算符接受一个(通常是非const的)右值引用;而拷贝版本则接受一个(通常是const的)普通左值引用。
左值引用(lvalue reference):可以绑定到左值的引用。右值引用(rvalue reference):指向一个将要销毁的对象的引用。
引用限定符(referencequalifier):用来指出一个非static成员函数可以用于左值或右值的符号。限定符&和&&应该放在参数列表之后或const限定符之后(如果有的话)。被&限定的函数只能用于左值;被&&限定的函数只能用于右值。
如果一个函数参数是指向模板参数类型的右值引用(如,T&&),则可以传递给它任意类型的实参。如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用(T&)。
右值引用是C++11中最重要的新特性之一,它解决了C++中大量的历史遗留问题,使C++标准库的实现在多种场景下消除了不必要的额外开销(如std::vector, std::string),也使得另外一些标准库(如std::unique_ptr,std::function)成为可能。即使你并不直接使用右值引用,也可以通过标准库,间接从这一新特性中受益。
右值引用的意义通常解释为两大作用:移动语义(Move Sementics)和完美转发(Perfect Forwarding)。它的主要目的有两个方面:(1)、消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率;(2)、能够更简洁明确地定义泛型函数。
右值引用可以使我们区分表达式的左值和右值。
右值引用主要就是解决一个拷贝效率低下的问题,因为针对于右值,或者打算更改的左值,我们可以采用类似与unique_ptr的move(移动)操作,大大的提高性能(move semantics)。另外,C++的模板推断机制为参数T&&做了一个例外规则,让左值和右值的识别和转向(forward)非常简单,帮助我们写出高效并且简捷的泛型代码(perfect forwarding)。
下面是从其他文章中copy的测试代码,详细内容介绍可以参考对应的reference:
#include "lvalue_rvalue.hpp"
#include
#include
#include
namespace lvalue_rvalue_ {
////////////////////////////////////////////////////////////
namespace {
char& get_val(std::string& str, std::string::size_type ix)
{
return str[ix];
}
}
int test_lvalue_rvalue_1()
{
{
// 赋值运算符的左侧运算对象必须是一个可修改的左值:
int i = 0, j = 0, k = 0; // 初始化而非赋值
const int ci = i; // 初始化而非赋值
// 下面的赋值语句都是非法的
//1024 = k; // 错误:字面值是右值
//i + j = k; // 错误:算术表达式是右值
//ci = k; // 错误:ci是常量(不可修改的)左值
}
{
int i = 0;
int& r = i; // 正确:r引用i
//int&& rr = i; // 错误:不能将一个右值引用绑定到一个左值上
//int& r2 = i * 42; // 错误:i*42是一个右值
const int& r3 = i * 42; // 正确:我们可以将一个const的引用绑定到一个右值上
int&& rr2 = i * 42; // 正确:将rr2绑定到乘法结果上
}
{
// 不能将一个右值引用绑定到一个右值引用类型的变量上
int&& rr1 = 42; // 正确:字面常量是右值
//int&& rr2 = rr1; // 错误:表达式rr1是左值
//int rr = &&rr1; // 不能将一个右值引用直接绑定到一个左值上
int i = 5;
int&& rr3 = std::move(i); // ok
}
// 调用一个返回引用的函数得到左值,其它返回类型得到右值
std::string s{ "a value" };
std::cout << s << std::endl; // a value
get_val(s, 0) = 'A';
std::cout << s << std::endl; // A value
return 0;
}
/////////////////////////////////////////////////////////
// reference: https://msdn.microsoft.com/en-us/library/f90831hc.aspx
int test_lvalue_rvalue_2()
{
int i, j;
int *p = new int;
// Correct usage: the variable i is an lvalue.
i = 7;
// Incorrect usage: The left operand must be an lvalue (C2106).
//7 = i; // C2106
//j * 4 = 7; // C2106
// Correct usage: the dereferenced pointer is an lvalue.
*p = i;
delete p;
const int ci = 7;
// Incorrect usage: the variable is a non-modifiable lvalue (C3892).
//ci = 9; // C3892
// Correct usage: the conditional operator returns an lvalue.
((i < 3) ? i : j) = 7;
return 0;
}
//////////////////////////////////////////////////////////
// reference: http://www.bogotobogo.com/cplusplus/C11/4_C11_Rvalue_Lvalue.php
namespace {
class cat {};
int square(int x) { return x*x; }
int square2(int& x) { return x*x; }
}
int test_lvalue_rvalue_3()
{
{ // lvalue examples
int i = 7; // i: lvalue
int *pi = &i; // i is addressable
i = 10; // we can modify it
class cat {};
cat c; // c is an lvalue for a user defined type
}
{ // rvalue examples
int i = 7; // i: lvalue but 7 is rvalue
int k = i + 3; // (i+3) is an rvalue
//int *pi = &(i + 3); // error, it's not addressable
//i + 3 = 10; // error - cannot assign a value to it
//3 = i; // error - not assignable
cat c;
c = cat(); // cat() is an rvalue
}
{
int sq = square(10); // square(10) is an rvalue
}
{
int i = 7;
square(i); // OK
square(7);
//square2(7); // error, 7 is an rvalue and cannot be assigned to a reference
}
{
int a = 7; // a is lvalue & 7 is rvalue
int b = (a + 2); // b is lvalue & (a+2) is rvalue
int c = (a + b); // c is lvalue & (a+b) is rvalue
int * ptr = &a; // Possible to take address of lvalue
//int * ptr3 = &(a + 1); // Compile Error. Can not take address of rvalue
}
return 0;
}
//////////////////////////////////////////////////////////
// reference: http://en.cppreference.com/w/cpp/language/reference
namespace {
void double_string(std::string& s)
{
s += s; // 's' is the same object as main()'s 'str'
}
char& char_number(std::string& s, std::size_t n)
{
return s.at(n); // string::at() returns a reference to char
}
}
int test_lvalue_rvalue_4()
{
// 1. Lvalue references can be used to alias an existing object (optionally with different cv-qualification):
std::string s = "Ex";
std::string& r1 = s;
const std::string& r2 = s;
r1 += "ample"; // modifies s
// r2 += "!"; // error: cannot modify through reference to const
std::cout << r2 << '\n'; // prints s, which now holds "Example"
// 2. They can also be used to implement pass-by-reference semantics in function calls:
std::string str = "Test";
double_string(str);
std::cout << str << '\n';
// 3. When a function's return type is lvalue reference, the function call expression becomes an lvalue expression
std::string str_ = "Test";
char_number(str_, 1) = 'a'; // the function call is lvalue, can be assigned to
std::cout << str_ << '\n';
return 0;
}
/////////////////////////////////////////////////////////
// reference: http://en.cppreference.com/w/cpp/language/reference
namespace {
void f(int& x)
{
std::cout << "lvalue reference overload f(" << x << ")\n";
}
void f(const int& x)
{
std::cout << "lvalue reference to const overload f(" << x << ")\n";
}
void f(int&& x)
{
std::cout << "rvalue reference overload f(" << x << ")\n";
}
}
int test_lvalue_rvalue_5()
{
// 1. Rvalue references can be used to extend the lifetimes of temporary objects
// (note, lvalue references to const can extend the lifetimes of temporary objects too, but they are not modifiable through them):
std::string s1 = "Test";
// std::string&& r1 = s1; // error: can't bind to lvalue
const std::string& r2 = s1 + s1; // okay: lvalue reference to const extends lifetime
// r2 += "Test"; // error: can't modify through reference to const
std::string&& r3 = s1 + s1; // okay: rvalue reference extends lifetime
r3 += "Test"; // okay: can modify through reference to non-const
std::cout << r3 << '\n';
// 2. More importantly, when a function has both rvalue reference and lvalue reference overloads,
// the rvalue reference overload binds to rvalues (including both prvalues and xvalues),
// while the lvalue reference overload binds to lvalues:
int i = 1;
const int ci = 2;
f(i); // calls f(int&)
f(ci); // calls f(const int&)
f(3); // calls f(int&&)
// would call f(const int&) if f(int&&) overload wasn't provided
f(std::move(i)); // calls f(int&&)
// This allows move constructors, move assignment operators, and other move-aware functions
// (e.g. vector::push_back() to be automatically selected when suitable.
return 0;
}
/////////////////////////////////////////////////////////////
// reference: http://www.bogotobogo.com/cplusplus/C11/5_C11_Move_Semantics_Rvalue_Reference.php
namespace {
void printReference(int& value)
{
std::cout << "lvalue: value = " << value << std::endl;
}
void printReference(int&& value)
{
std::cout << "rvalue: value = " << value << std::endl;
}
int getValue()
{
int temp_ii = 99;
return temp_ii;
}
}
int test_lvalue_rvalue_6()
{
int ii = 11;
printReference(ii);
printReference(getValue()); // printReference(99);
return 0;
}
////////////////////////////////////////////////////////////
// reference: https://msdn.microsoft.com/en-us/library/dd293668.aspx
namespace {
template struct S;
// The following structures specialize S by
// lvalue reference (T&), const lvalue reference (const T&),
// rvalue reference (T&&), and const rvalue reference (const T&&).
// Each structure provides a print method that prints the type of
// the structure and its parameter.
template struct S {
static void print(T& t)
{
std::cout << "print: " << t << std::endl;
}
};
template struct S {
static void print(const T& t)
{
std::cout << "print: " << t << std::endl;
}
};
template struct S {
static void print(T&& t)
{
std::cout << "print: " << t << std::endl;
}
};
template struct S {
static void print(const T&& t)
{
std::cout << "print: " << t << std::endl;
}
};
// This function forwards its parameter to a specialized
// version of the S type.
template void print_type_and_value(T&& t)
{
S::print(std::forward(t));
}
// This function returns the constant string "fourth".
const std::string fourth() { return std::string("fourth"); }
}
int test_lvalue_rvalue_7()
{
// The following call resolves to:
// print_type_and_value(string& && t)
// Which collapses to:
// print_type_and_value(string& t)
std::string s1("first");
print_type_and_value(s1);
// The following call resolves to:
// print_type_and_value(const string& && t)
// Which collapses to:
// print_type_and_value(const string& t)
const std::string s2("second");
print_type_and_value(s2);
// The following call resolves to:
// print_type_and_value(string&& t)
print_type_and_value(std::string("third"));
// The following call resolves to:
// print_type_and_value(const string&& t)
print_type_and_value(fourth());
return 0;
}
} // namespace lvalue_rvalue_