C++11学习笔记3

右值引用

分类

在古老的标准里,C++中的变量分为左值(lvalue)与右值(rvalue)这两种,左值就是能够用&获得地址的值,可以对他进行修改,右值就是不能用&获得地址的值,通常只是临时变量,不能进行修改。而在C++11中,变量不再仅仅分为左值与右值了,他引入了另一种值叫将亡值(expire value,xvalue)。从此,变量类型分为了三种:

  • lvalue :left value 就是传统意义上的左值
  • xvalue :expire value 就是将亡值
  • prvalue :pure right value 就是传统意义上的右值

而且,xvalue+prvalue又称为rvalue,lvalue+xvalue又称为glvalue。

定义

那么什么是xvalue呢,什么又是右值引用呢?
xvalue其实就是对右值的引用:

1
int &&x=10;

这个x跟传统的左值引用肯定是不一样的,毕竟左值引用是不能引用右值的。
这个x跟左值变量也是不一样的,虽然看上去没啥区别,但是实际上这个x并没有进行构造,而是像左值引用一样,对右值10进行了引用,使得这个右值的内存不被立即释放。这样我们就可以像使用左值一样的使用这个右值了。
那么现在就应该清楚了,右值引用就是通过对右值进行引用使得我们能够保存这个右值的生命周期,并像使用左值一样的使用右值的方法。(自己的定义)

用途

这么费劲心机定义一个右值引用有啥意义呢?其实主要是为了提高变量传递的效率。虽然很多情况下,我们的编译器会做一些优化,但是不同编译器不同,因此下面基于g++的测试我开启了-fno-elide-constructors参数防止其优化。
比如下面的这个过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
class Test{
public:
Test(){
std::cout<<"construct"<<std::endl;
}
Test(const Test &test){
std::cout<<"copy"<<std::endl;
}
~Test(){
std::cout<<"destroy"<<std::endl;
}
};
int main(){
Test t=Test();
}

看上去没什么额外开销,而事实上,这个过程的执行结果是:

1
2
3
4
construct
copy
destroy
destroy

也就是说他执行了一次构造,一次拷贝构造。而事实上,这个拷贝构造是浪费额外开销的,而且这个拷贝其实是对一个右值的拷贝,在拷贝后这个右值就被析构了,我们完全可以不执行析构而让新的值就用这个右值的引用。
旧的做法是使用常量引用来做这件事:

1
2
3
int main(){
const Test& t=Test();
}

这避免了额外的拷贝,但是后果是这个值只能是常量,无法被修改。
如果使用了右值引用,那么这个问题就简单多了:

1
2
3
int main(){
Test&& t=Test();
}

结果:

1
2
construct
destroy

这个右值引用既没有对结果进行拷贝,也能够让我们像使用一个左值一样的使用他。

移动拷贝构造

为什么引入移动拷贝构造,这是因为我们考虑到了以下问题:
我们知道,构造函数分为深拷贝和浅拷贝,默认的是浅拷贝,浅拷贝导致的结果就是拷贝出来的对象跟拷贝前的对象拥有相同的堆内元素,如果我们析构了拷贝前的对象,那么拷贝后的对象就无法使用了,因此浅拷贝不适合做赋值的移动操作;而深拷贝呢,又太浪费空间了,完全没有必要生成一个一模一样的对象然后把原先的再删除。因此就引入了拷贝构造这个东西。
比如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Test{
private:
int *ptr;
public:
Test():ptr(new int(0)){
std::cout<<"construct"<<std::endl;
}
Test(const Test &test):ptr(new int(*test.ptr)){
std::cout<<"copy"<<std::endl;
}
Test(Test &&test):ptr(test.ptr){
test.ptr=nullptr;
std::cout<<"move"<<std::endl;
}
~Test(){
delete ptr;
std::cout<<"destroy"<<std::endl;
}
};
int main(){
Test t=Test();
}

结果:

1
2
3
4
construct
move
destroy
destroy

移动构造的参数就是一个右值引用,他做的事是将移动源的所有堆内元素指针断开,并连上移动目的地的指针。这样移动源就可以安心的析构而不影响移动目的地的堆内元素。同时也省去了不必要的拷贝开销,效率非常的高。

std::move

移动构造有一个问题,就是他的参数必须是右值,这就带来一个问题,如果上面的main函数变成这样:

1
2
3
4
int main(){
Test t1;
Test t2=t1;
}

结果:

1
2
3
4
construct
copy
destroy
destroy

由于t1是个左值,那么他会去执行拷贝构造而不会执行移动构造,这显然不是我们像看到的。
为了使左值的移动能够使用移动构造,我们就有了std::move这个东西,他的作用很简单,就是把左值变成右值引用。

1
2
3
4
int main(){
Test t1;
Test t2=std::move(t1);
}

结果:

1
2
3
4
construct
move
destroy
destroy

如我所愿。
注意一点就是在使用std::move()之后的对象t1这时候的堆内元素就已经无效了。

std::move()这么好用,显然c++中的模板都支持这个move语义。

std::emplace_back

我们知道了对象的构造,拷贝、移动都是要付出一些代价的,那我们很容易就会想到,当我们使用容器的时候,如果使用拷贝构造,显然效率很低;如果使用移动构造,那么还是会有一些移动的开销,需要执行移动构造函数,能不能有一种方法,直接在容器里执行构造函数,这样就既不用拷贝,也不用移动了呢?答案是当然有,这就是std::emplace_back方法。我们可以比较下面的三种方法:

拷贝插入

1
2
3
4
5
int main(){
std::vector<Test>v;
Test t;
v.push_back(t);
}

输出:

1
2
3
4
construct
copy
destroy
destroy

移动插入

1
2
3
4
int main(){
std::vector<Test>v;
v.push_back(Test());
}

输出:

1
2
3
4
construct
move
destroy
destroy

emplace_back

1
2
3
4
int main(){
std::vector<Test>v;
v.emplace_back();
}

输出:

1
2
construct
destroy

显然,这个emplace_back的效率是最高的。

参考资料

深入应用c++11