左右值引用和移动语义
文章首发公众号:iDoitnow
1. 左右值和左右值引用
什么是左值、右值呢?一种极不严谨的理解为:在赋值的时候,能够被放到等号左边的值为左值,放在右边的值为右值。例如:
int sum(int x, int y){return x + y;}
int a = 1; //a为左值,常数1为右值
int b = a + a; //b为左值,表达式a+a为右值
int c = sum(a, a);//c为左值,但函数sum(a, a)返回值为右值
通过上面的例子,常数a
、表达式(a+a)
和函数sum(a+a)
返回值他们都是临时值,这些值都保存在寄存器中,无法取到他们的地址;而对于a
、b
和c
为具体的变量名,存储在内存中,可以取到其地址。因此一般情况下可以根据能否取到地址,来区分左值和右值。
在了解左值和右值之前,我们首先要知道表达式的概念:由运算符和运算对象构成的计算式(类似数学中的算术表达式)。表达式是可以求值的,因此根据表达式值的类别,可以对其进行分类(准确的来说,是表达式的结果的值类别,但我们一般不刻意区分表达式和表达式的求值结果,所以这里称“表达式的值类别”。),C++11之后将表达式定义了五种类型:
-
lvalue(Left-hand-side value,左值)
-
prvalue(Pure rvalue,纯右值)
-
xvalue(eXpiring value,将亡值)
-
rvalue(Right-hand-side value,右值)
-
glvalue(Generalized lvalue,泛左值)
它们之间的关系如下图所示:
值的分类C++11中将表达式按值类别可以分为左值、将亡值和纯右值。其中,左值和将亡值合称为泛左值,纯右值和将亡值合称为右值。
随着移动语义(后面我们会详细介绍)引入到 C++11 之中,值类别被重新进行了定义,C++之父Bjarne Stroustrup在《“New” Value Terminology》中给出以区别表达式的两种独立的性质:
- 拥有身份 (identity):可以确定表达式是否与另一表达式指代同一实体,例如通过比较它们所标识的对象或函数的(直接或间接获得的)地址;
- 可被移动:移动构造函数、移动赋值运算符或实现了移动语义的其他函数重载能够绑定于这个表达式。
C++11 中:
- 拥有身份且不可被移动的表达式被称作左值 (lvalue)表达式;
- 拥有身份且可被移动的表达式被称作将亡值 (xvalue)表达式;
- 不拥有身份且可被移动的表达式被称作纯右值 (prvalue)表达式;
1.1 左值
一般情况下,左值我们可以简单地理解理解为:能够使用&
取地址的表达式。
常见的左值有:
- 变量名
- 函数名
- 返回左值引用的函数调用
- 前置自增/减的运算符链接的表达式(如
++i
/--i
) - 内置的赋值表达式(如
a=b
,a+=1
) - 字符串等。
【注:字符串是可以取地址的,因此字符串常量也属于左值】
1.2 纯右值
纯右值:表达式本身就是纯粹的字面值(如1
,ture
,1.0
);或者,该表达式求值结果相当于一个字面值或一个不具名的临时对象。
常见的纯右值有:
- 除字符串字面值以外的字面值
- 返回非引用类型的函数调用
- 后置自增/减的运算符链接的表达式(如
i++
/i--
) - 算术/逻辑/比较表达式(如
a+b
,a&&b
,a==b
) - 取地址表达式(如
&a
)
1.3 将亡值
将亡值是在C++11中引进来的,顾名思义,就即将销毁的东西。将亡值的产生与右值引用的产生而引起的,对于将亡值我们常用到的有:
- 返回类型是右值引用的函数调用或重载运算符的表达式(如
std::move(x)
) - 转换为右值引用的转换函数的调用表达式(如
static<int&&>(a)
)
1.4 左右值引用
左值引用就是对左值的引用。它的形式如:T&
,根据const属性可以分为两种:
- const左值引用
- 非const左值引用
例如:
int a = 1;
int& la = a;//la为a的左值引用(非const左值引用)
la = 2;//la为非const左值引用,可以修改它的值
const int& c_la = a;//c_la为a的左值引用(const左值引用)
c_la = 2;//该语法错误,c_la为const左值引用,不可以修改它的值
右值引用就是对右值的引用,通过T&&
来表示。右值的引用只能绑定到右值上。
2. 移动语义
在未出现右值引用之前,我们在函数调用传参的时候,在某些时候可以使用按引用传递参数,减少参数多的拷贝对资源的消耗,提高程序的运行效率。当我们在处理包含大量数据的对象时,移动语义显的尤为重要。
2.1 std::move
如何将一个左值转换为一个右值呢?C++11在头文件utility中声明了std::move()
函数,该函数的作用就是类型转换,通过它,我们可以 把一个左值,将其标记为右值。move()
不做任何资源转移的操作,只是产生一个将亡值表达式来标识参数x
,其完全等同于static_cast<T&&>(x)
。例如:
int a = 1;
int&& r_a = a; //错误,右值引用只能绑定到右值上,而a是一个左值
int&& r_a = std::move(b); //正确, std::move(a) 是一个右值,可以用右值引用绑定
2.2 移动构造函数
一个类 T
的首个形参是 T&&、const T&&
、volatile T&&
或 const volatile T&&
,且没有其他形参,或剩余形参都有默认值。
具体的形式如下:
T (T &&) //移动构造函数的典型声明形式
T (T &&) = default; //强制编译器生成移动构造函数。
T (T &&) = delete; //避免隐式生成移动构造函数。
示例:
#include <string>
#include <iostream>
#include <utility>
class A
{
private:
std::string s;
public:
A(std::string str = "A()") : s(str) {std::cout<<s<<"的构造函数\n";}
A(A&& o) : s(std::move(o.s)) {std::cout<<s<<"的移动构造函数\n";}
~A(){std::cout<<s<<"的析构函数\n";}
};
A f(A a) { return a; }
int main()
{
A a1(f(A("a")));// 按值返回时,从函数形参移动构造它的目标
A a2(std::move(a1));// 从亡值移动构造
}
2.3 移动赋值运算符
一个类 T
的移动赋值运算符是名为 operator=的非模板非静态成员函数,它接受恰好一个 T&&
、const T&&
、volatile T&&
或 const volatile T&&
类型的形参。
具体的形式如下:
T & T ::operator= (T &&) //移动赋值运算符的典型声明
T & T ::operator= (T &&) = default; //强制编译器生成移动赋值运算符
T & T ::operator= (T &&) = delete; //避免隐式移动赋值
示例:
#include <string>
#include <iostream>
#include <utility>
class A
{
private:
std::string s;
public:
A(std::string str = "A()") : s(str) {std::cout<<s<<"的构造函数\n";}
~A(){std::cout<<s<<"的析构函数\n";}
A& operator=(const A& other)
{
s = other.s;
std::cout << "复制赋值\n";
return *this;
}
A& operator=(A&& other)
{
s = std::move(other.s);
std::cout << "移动赋值\n";
return *this;
}
};
A f(A a) { return a; }
int main()
{
A a1("a1"), a2("a2");
std::cout << "尝试从右值临时量移动赋值 A\n";
a1 = f(A("a")); // 从右值临时量移动赋值
std::cout << "尝试从亡值移动赋值 A\n";
a2 = std::move(a1); // 从将亡值移动赋值
}
参考文献
C++ Primer Plus(第六版) - 第18章 探讨C++新标准
C++ 参考手册