1 左值(lvalue)和右值(rvalue)
左右值的两个定义:
1:位于赋值运算符 = 右边的值为右值;左边的为左值。
2:左值可以取得地址、有名字;不可以取得地址、没有名字的为右值。
A a = foo(); // foo() 为右值
char *x = "thu"; // “thu”为字面值也为右值
a = b + c; // b + c这个结果也是一个右值
所以 A a = foo()可以用 &a取得a的地址,a 是左值;不能取得 foo()的地址, foo()返回的临时对象也是没有名字的,所以 foo() 是右值。
在C++11中,右值包括两种,一中是将亡值(xvalue, eXpiring Value),一种是纯右值(prvalue,Pure Rvalue)。函数非引用返回的临时对象、运算表达式的结果1、 3.14、'c'这样的字面值都属于纯右值。而将亡值则是由 C++11引入的,当该右值完成初始化或赋值的任务时,它的资源已经移动给了被初始化者或被赋值者,同时该右值也将会马上被销毁(析构)。也就是说,当一个右值准备完成初始化或赋值任务时,它已经“将亡”了。与右值引用相关的表达式,如:将要被移动的对象、T&&函数返回值、std::move返回值和转换为 T&&的类型的转换函数的返回值等。
1)返回右值引用的函数的调用表达式
2)转换为右值引用的转换函数的调用表达式
- 字符串字面值是左值:
不是所有的字面值都是纯右值,字符串字面值是唯一例外。
早期C++将字符串字面值实现为char型数组,实实在在地为每个字符都分配了空间并且允许程序员对其进行操作,所以类似。
cout<<&("abc")<<endl;
char *p_char="abc";//注意不是char *p_char=&("abc");
-
解引用表达式p是左值,取地址表达式&a是纯右值:
&(p)一定是正确的,因为p得到的是p指向的实体,&(p)得到的就是这一实体的地址,正是p的值。由于&(p)的正确,所以p是左值。而对&a而言,得到的是a的地址,相当于unsigned int型的字面值,所以是纯右值。 -
++i是左值,i++是右值。
前者,对i加1后再赋给i,最终的返回值就是i,所以,++i的结果是具名的,名字就是i;而对于i++而言,是先对i进行一次拷贝,将得到的副本作为返回结果,然后再对i加1,由于i++的结果是对i加1前i的一份拷贝,所以它是不具名的。
2 左值引用和右值引用
左值引用就是一般的引用,一般用一个&表示,例如
const A &a_ref = a; // 取得对象 a 的引用
右值引用顾名思义,就是右值的引用, 用 &&表示
A &&a = getRvalue(); // a是一个右值引用
A b = getRvalue();
右值引用也相当于别名,与左值的区别为右值引用是无名变量的别名。
getRvalue() 是一个返回右值的函数,右值在这一句执行完就该结束他的生存期了,如果是对象就该调用析构函数了;但是右值引用让它强行续命;使用右值引用指向右值,右值的生存期和右值引用一样长了, 相比较直接赋值少一次对象的析构和构造
C++的右值引用主要有两个用处,一个是移动语义
,一个是完美转发
。
3 移动语义(move)
有的类是可以拷贝的,可以调用拷贝构造函数,有点类的对象则是独一无二的,或者类的资源是独一无二的,比如 IO 、 std::unique_ptr等,它们不可以复制,但是可以把资源交出所有权给新的对象,称为可以移动的。
C++11最重要的一个改进之一就是引入了move语义,这样在一些对象的构造时可以获取到已有的资源(如内存)而不需要通过拷贝,申请新的内存,这样移动而非拷贝将会大幅度提升性能。例如有些右值即将消亡析构,这个时候我们用移动构造函数可以接管他们的资源。
移动构造函数
class Moveable{
public:
Moveable():i(new int[3]){
cout<<"construct!"<<endl;
}
Moveable(const Moveable & m):i(new int[*m.i]){
cout<<"copy!"<<endl;
}
Moveable(Moveable && m):i(m.i){
cout<<"move!"<<endl;
m.i = nullptr;
}
~Moveable(){
cout<<"destroy!"<<endl;
delete i;
}
int* i;
};
Moveable getM(){
return Moveable();
}
int main()
{
cout<<"1............"<<endl;
Moveable a;
cout<<"2............"<<endl;
Moveable b(a); //copy
cout<<"3............"<<endl;
Moveable c = a; //copy
cout<<"4............"<<endl;
Moveable d = getM(); //两个move
cout<<"5............"<<endl;
Moveable f(move(a)); //move
return 0;
}
编译时为了看到临时对象拷贝我们关闭了编译器省略复制构造的优化
g++ main.cpp -o main -fno-elide-constructors -std=c++11
执行结果
1............
construct!
2............
copy!
3............
copy!
4............
construct!
move!
destroy!
move!
destroy!
5............
move!
destroy!
destroy!
destroy!
destroy!
移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。意味着,移动构造函数的参数是一个右值或者将亡值的引用。只用用一个右值或者将亡值初始化另一个对象的时候,才会调用移动构造函数。
使用getM()函数返回的临时对象初始化d,由于临时对象是右值,所以调用移动构造函数。这里调用了两次移动构造函数。第一次是getM()返回前,Moveable()移动构造了一个临时对象。第二次是临时对象移动构造d。
std::move()
std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值。
int lv = 4;
int &lr = lv; // 正确,lr是l的左值引用
int &&rr = lv; // 错误,不可以把右值引用绑定到一个左值
int &&rr = std::move(lv); // 正确,把左值转换为右值
std::move ()的使用举例
void swap(A &a1, A &a2){
A tmp(a1); // 拷贝构造函数一次,涉及大量数据的拷贝
a1 = a2; // 拷贝赋值函数调用,涉及大量数据的拷贝
a2 = tmp; // 拷贝赋值函数调用,涉及大量数据的拷贝
}
void swap_A(A &a1, A &a2){
A tmp(std::move(a1)); // a1 转为右值,调用移动构造函数,低成本
a1 = std::move(a2); // a2 转为右值,调用移动赋值函数,低成本
a2 = std::move(tmp); // tmp 转为右值,调用移动赋值函数
}
std::move ()的使用举例
#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main()
{
std::string str = "Hello";
std::vector<std::string> v;
//调用常规的拷贝构造函数,新建字符数组,拷贝数据
v.push_back(str);
std::cout << "After copy, str is \"" << str << "\"\n";
//调用移动构造函数,掏空str,掏空后,最好不要使用str
v.push_back(std::move(str));
std::cout << "After move, str is \"" << str << "\"\n";
std::cout << "The contents of the vector are \"" << v[0] << "\", \"" << v[1] << "\"\n";
}
执行结果
After copy, str is "Hello"
After move, str is ""
The contents of the vector are "Hello", "Hello"
4 完美转发(perfect forwarding)
所谓转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。
4.1 万能引用
template<typename T>
void func(T& param) {
cout << "传入的是左值" << endl;
}
template<typename T>
void func(T&& param) {
cout << "传入的是右值" << endl;
}
int main() {
int num = 2019;
func(num);
func(2019);
return 0;
}
输出结果:
传入的是左值
传入的是右值
第一次函数调用的是左值得版本,第二次函数调用的是右值版本。但是,有没有办法只写一个模板函数即可以接收左值又可以接收右值呢?C++ 11中有万能引用(Universal Reference)的概念:使用T&&类型的形参既能绑定右值,又能绑定左值。
所以,上面的案例我们可以修改为:
template<typename T>
void func(T&& param) {
cout << param << endl;
}
int main() {
int num = 2019;
func(num);
func(2019);
return 0;
}
4.2 完美转发
下面接着说完美转发(Perfect Forwarding),首先,看一个例子:
template<typename T>
void func(T& param) {
cout << "传入的是左值" << endl;
}
template<typename T>
void func(T&& param) {
cout << "传入的是右值" << endl;
}
template<typename T>
void warp(T&& param) {
func(param);
}
int main() {
int num = 2019;
warp(num);
warp(2019);
return 0;
}
输出结果:
传入的是左值
传入的是左值
warp()函数本身的形参是一个万能引用,即可以接受左值又可以接受右值;
第一个warp()函数调用实参是左值,所以warp()函数中调用func()中传入的参数也应该是左值;
第二个warp()函数调用实参是右值,warp()函数接收的参数类型是右值引用,那么为什么却调用了调用func()的左值版本了呢?这是因为在warp()函数内部,左值引用类型变为了右值,因为参数有了名称。
怎么保持函数调用过程中,变量类型的不变呢?这就是完美转发,在C++11中通过std::forward()函数来实现。修改我们的warp()函数如下:
template<typename T>
void warp(T&& param) {
func(std::forward<T>(param));
}
1:forward<T>(x) 能够将x的类型变成 T&&, 即 forward<T> 返回类型是 T&&。
2:当 warp收到一个类型为int 的左值/左值引用时, 由于引用折叠,T的类型为int&, func收到的类型为int &&& --> int&,一个左值引用。
2:当 warp收到一个类型为int 类型的右值时, T 被推断成 int, 从而 std::forward<int> 返回的类型为 int&&, func正确的收到了右值参数。
参考
https://www.cnblogs.com/sunchaothu/p/11343517.html
https://www.jianshu.com/p/d19fc8447eaa
网友评论