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<intget() {
  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<intget(int v) {
  if (v < 0) {
    return std::vector<int>();
  }
  else {
    return std::vector<int>(111);
  }
}

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<intget(int v);

for (auto &it : get(1)) {
  // do something
}

4.3 常左值引用

即const引用,const引用算是一个万能引用,可以接受基本上任何类别数据,同时

#include <vector>
std::vector<intget(int v) {
  if (v < 0) {
    return std::vector<int>();
  }
  else {
    return std::vector<int>(111);
  }
}

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++之右值引用&移动语义┇移动构造&移动复制

是否可以在 C++ 中将返回值移动语义与受保护的复制构造函数一起使用?

c++的左值(lvalue),右值(rvalue),移动语义(move),完美转发(forward)

一文入魂:再也不用担心我不懂C++移动语义了!

一文入魂:妈妈再也不用担心我不懂C++移动语义了!

C++ 专题 右值引用移动语义与完美转发