c++ 移动语义及使用
Posted 0号程序员
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c++ 移动语义及使用相关的知识,希望对你有一定的参考价值。
1 前置
c++11引入了右值引用和移动语义,可以避免一些拷贝,提高程序运行的性能。本文我打算简单介绍下左值右值及相关的概念,着重介绍如何使用移动语义,为什么使用以及怎么使用
2 左值和右值
看一条简单的赋值语句
int a = 0;
int get() {
return 0;
}
// a是左值,get()返回也是右值
int a = get();
但是事实却比这复杂一点,右值分为纯右值和将亡值,广义左值分为左值和将亡值。
即一个表达式必定属于左值,纯右值,将亡值中的一个。
对比起来我们没有说到就是将亡值:
int u = 1;
int &&uu = std::move(u);
而这里std::move(u)就是将亡值,可能会想都是函数返回值,为啥这个就是将亡值,其他的就是纯右值呢,主要是这个函数返回值类型是右值引用:
template <class _Tp>
typename remove_reference<_Tp>::type&&
move(_Tp&& __t) _NOEXCEPT
{
typedef _LIBCPP_NODEBUG_TYPE typename remove_reference<_Tp>::type _Up;
return static_cast<_Up&&>(__t);
}
至于为什么,我们后边再讲。
3 左值引用和右值引用
指向左值的引用,我们称为左值引用,指向右值的引用,我们称为右值引用。右值引用使用&&来表示
大家有时候会被左值,左值引用,右值,右值引用这几个概念搞的晕头转向,左值和右值这个属于值的类别,左值引用和右值引用这样讲是说的他的类型,都是引用类型。
举一个简单的例子:
int &&a = 1;
4 为什么要用移动语义
C++中长久以来存在为人所诟病的临时对象效率问题 我们来看一个例子:
#include <vector>
std::vector<int> get() {
std::vector<int> vct;
return vct;
}
std::vector<int> a = get();
这里最开始的c++,具体执行细则是这样的,会出现两次拷贝,第一次把vct拷贝给返回值的临时变量,第二次临时变量拷贝给a,然后继而出现两次vector的析构。看起来很蠢是吧。
4.1 (N)RVO(Name)(Return Value Optimization)
不过大家可能会说,有编译器优化呀,即(N)RVO(Name)(Return Value Optimization),函数返回值优化,其实只要在函数里加上一些逻辑,这个优化就部分失效了
#include <vector>
std::vector<int> get(int v) {
if (v < 0) {
return std::vector<int>();
}
else {
return std::vector<int>(1, 11);
}
}
std::vector<int> a = get(1);
4.2 左值引用
继而就出现了引用这个概念,首先出现左值引用的概念,以上代码我们就可以使用如下方式来写:
#include <vector>
void get(int v, std::vector<int>& vec) {
if (v < 0) {
vec.clear();
}
else {
vec.push_back(1);
}
}
std::vector<int> a;
get(1, a);
直接针对原对象操作,方便了许多。这个写法还是有弊端,看起来并不优雅,比如说我要这样写一个代码,
void get(int v, std::vector<int>& vec);
std::vector<int> a;
get(1, a);
for (auto &it : a) {
// do something
}
我们遍历这个vector需要写成这样,但是实际上我们可以写成下边这样是不是更优雅,那么上边的那种写法,我们无法做到。
std::vector<int> get(int v);
for (auto &it : get(1)) {
// do something
}
4.3 常左值引用
即const引用,const引用算是一个万能引用,可以接受基本上任何类别数据,同时
#include <vector>
std::vector<int> get(int v) {
if (v < 0) {
return std::vector<int>();
}
else {
return std::vector<int>(1, 11);
}
}
const std::vector<int>& a = get(1);
如果写成这样,就可以达到优化的效果,再加上(N)RVO,a作为返回值的引用,避免了两次的拷贝,不过这样的局限性就是const,获得的vector的操作也只能是const。
4.4 再来看一个例子
假设你写了一个容器,类似vector的,我们姑且叫他lvector,同时我这里有一个Obj,你的lvector存放obj
class Obj {
public:
Obj() : ptr_(new int[10]{0}) {}
~Obj() {
delete []ptr_;
}
int* ptr_;
};
// eg1. 临时对象
Obj getObj(int v) {
if (v < 1) {
return Obj();
}
else {
Obj o;
o.ptr_[0] = 10;
return o;
}
}
// eg2. 具名对象
Obj o2;
// Pseudo code
class lvector {
public:
void push_back(xxx) {xxx}
Obj* obj_arr_[10];
int index_ = 0;
};
lvector obj_vec;
obj_vec.push_back(getObj(1));
obj_vec.push_back(o2);
如果是你来写,这个push_back的参数和实现怎么来写呢,我们分别来看下:
// 1
void push_back(Obj o) {
obj_arr_[index_++] = new Obj(o);
}
// 2
void push_back(Obj& o) {
obj_arr_[index_++] = new Obj(o);
}
// 3
void push_back(const Obj& o) {
obj_arr_[index_++] = new Obj(o);
}
先看第一个,参数直接声明,首先传递参数时进行了一次拷贝构造,复制到数组里边又进行了一次拷贝构造,而且这里的拷贝构造,我们还需要在Obj对象中写拷贝构造函数,因为这里要深拷贝,避免多次析构共有资源了。
第二个和第三个只进行了一次拷贝构造,且第三个可以接受临时对象。
针对临时变量来说,那么有没有办法也不要这次拷贝呢,如果具名对象只是的push_back后就不在使用了,是不是也可以不进行这一次拷贝构造。
4.5 移动语义
答案是肯定的,其实在此之前出现这些问题的本质是什么,C++没有区分copy和move语意,有了移动语义上边的代码就可以写成:
class lvector {
public:
void push_back(const Obj& o) {
obj_arr_[index_++] = new Obj(o);
}
void push_back(Obj&& o) {
obj_arr_[index_++] = new Obj(std::move(o));
}
Obj* obj_arr_[10];
int index_ = 0;
};
我们写了两个互为重载的函数,一个用来接收真的需要拷贝的obj(左值),而另一个则接收右值。不过Obj对象需要加上拷贝构造函数和移动构造函数,第一个push_back会用到拷贝构造函数,我们要写一个深拷贝构造函数,第二个push_back会用到移动构造函数,我们这里写一下Obj的移动构造函数和拷贝构造函数:
class Obj {
public:
Obj() : ptr_(new int[10]{0}) {}
~Obj() {
delete []ptr_;
}
Obj(const Obj& o) {
ptr_ = new int[10]{0};
for (int i= 0; i < 10; ++i) {
ptr_[i] = o.ptr_[i];
}
}
Obj(Obj&& o) {
ptr_ = o.ptr_;
o.ptr_ = nullptr;
}
int* ptr_;
};
有了一些前置条件,我们就可以使用了,这样我们使用方式就是:
//snap...
lvector obj_vec;
// 1 纯右值
obj_vec.push_back(getObj(1));
// 2 将亡值
{
Obj o2;
obj_vec.push_back(std::move(o2));
}
// 左值
Obj o3;
obj_vec.push_back(o3);
第一种就是我们使用返回值这样的纯右值来作为参数传递,我们调用的是右值引用的那个push_back, 第二个因为我们之后基本不会用到o2了,他传达出来的语义就是把o2的资源传递到obj_vec,第三个就是把o3的值拷贝一份到obj_vec。相信大家到这里应该已经看出了为什么会有移动语义,总结下就是在C++11之前不能完成这个区分移动和拷贝。出于效率,即使我们用了左值引用和常左值引用有些功能受限甚至无法做到。
篇幅影响,如何使用移动语义见下文
以上是关于c++ 移动语义及使用的主要内容,如果未能解决你的问题,请参考以下文章
是否可以在 C++ 中将返回值移动语义与受保护的复制构造函数一起使用?