博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
C++11线程指南(4)--右值引用与移动语义
阅读量:4071 次
发布时间:2019-05-25

本文共 5482 字,大约阅读时间需要 18 分钟。

目录


1. 按值传递

  什么是按值传递?

  当一个函数通过值的方式获取它的参数时,就会包含一个拷贝的动作,编译器知道如何去进行拷贝。如果参数是自定义类型,则我们还需要提供拷贝构造函数,或者赋值运算符来进行深拷贝。
  然而,拷贝是需要代价的。在我们使用STL容器时,就存在大量的拷贝代价。当按值传递参数时,会产生临时对象,浪费宝贵的CPU以及内存资源。
  需要找到一个减少不必要拷贝的方法。移动语义就是其中一种。

2. 右值引用

  此处介绍右值引用的目的,是为了实现后面的移动语义。

  右值引用可以帮助我们分辨一个值是左值还是右值。C++11标准规定右值引用只能绑定右值,它使用两个&符号进行声明。

int&& rvalue_ref = 99;

  下面是一个右值引用例子:

#include
void f(int& i) { std::cout<<"lvalue ref: "<
<

  运行结果为:

  lvalue ref: 99
  rvalue ref: 99
  rvalue ref: 99
  其中std::move函数的作用就是将一个左值转换为成右值。
  如果一个表达式会生成一个临时对象,则它就是一个右值。如下:

#include
int getValue() { int i = 22; return i;}int main() { std::cout<
<

  getValue()就是一个右值。注意:返回的这个值并不是i的引用,而是一个临时值。

  在C++0x中,使用左值引用仅能绑定const类型的临时对象。

const int& val = getValue(); //OKint& val = getValue();       //Wrong!

  但是,C++11中的右值引用允许我们绑定一个mutable引用到rvalue,但不是lvalue。换言之,右值引用可以完美的判断一个值是否为临时对象。

const int&& val = getValue(); //OKint&& val = getValue();       //OK

  下面进行一下比较:

void printReference (const int& value){    std::cout << value;}void printReference (int&& value){    std::cout << value;}

  第一个函数的参数类型为const lvalue, 它可以接收任何传入参数,无论是rvalue还是lvalue. 但是,第二个函数只能接收右值引用。

  换言之,我们可以使用函数重载,即一个使用左值引用参数,另一个使用右值引用参数,来判断传入的参数是左值还是右值。也就是说,C++11引入了一个新的类型, non-const reference, 即右值引用,声明方式为T&&。它代表一个初始化后还允许被修改的临时值。这也是移动语义的基础。

#include
void printRef(int& value) { std::cout<<"lvalue: value = "<
<

  运行结果为:

  lvalue: value = 11
  rvalue: value = 88
  注意:第一个printRef函数中,参数没有const修饰符,这样使得它只能接受左值。
  到此为止,我们可以写出两个区别明显的重载函数:一个只接受左值参数,另一个只接受右值参数。有何好处呢?它给予了我们一种以更少代码实现更有效率程序的方法!
  
  对右值引用的总结:
  1)int&& a: C++11中的新类型-右值引用采用的声明方式
  2)non-const 左值引用绑定到一个对象
  3)右值引用绑定到一个通常不会再被使用的临时对象

3. 移动语义

  在C++03中,如果参数按值传递,就会隐含一个不必要的深拷贝代价在里边。我们可以使用右值引用来避免深拷贝带来的性能损失。

  基于前面的论述,我们已经有了一种可以用来判断是临时对象还是永久对象的方法。现在的问题是,如何使用它呢?
  右值引用的主要作用就是用来创建移动构造函数(move constructor)以及移动赋值运算符(move assignment operator)。移动构造函数,类似于拷贝构造函数,使用对象实例做为参数并且基于原始对象来创建一个新的实例。只是,移动构造函数可以避免内存重新分配,因为我们知道它提供了一个临时对象。
  换言之,右值引用和移动语义避免了不必要的临时对象拷贝,我们无需拷贝临时对象。这样,临时对象所需的资源,可以用于其它对象。
  右值通常是临时的并且可以被修改的。如果我们知道函数参数是一个右值,可以把它当作临时存储使用,或者获取其内容,而不会改变程序的输出。这意味着我们可以移动其内容,而无需拷贝其内容。这样节省了大量的内容分配,并可对大量动态内存结构程序进行优化。
  下面是一个典型的使用移动语义的类定义:

#include
#include
#include
class Dummy {public: explicit Dummy(size_t length):_length(length), _data(new int[length]) { std::cout<<"Dummy(size_t).length = "<<_length<<"."<
vec; vec.push_back(Dummy(55)); vec.push_back(Dummy(77)); //插入一个新的元素至vector的第二个位置。 vec.insert(vec.begin()+1, Dummy(66)); return 0;}

  运行结果为:

Dummy(size_t).length = 55.
Dummy(Dummy&&).length = 55. Moving resource.
~Dummy().length = 0.
Dummy(size_t).length = 77.
Dummy(Dummy&&).length = 77. Moving resource.
Dummy(const &Dummy).length = 55.Copying resource.
~Dummy().length = 55.
delete resource.
~Dummy().length = 0.
Dummy(size_t).length = 66.
Dummy(Dummy&&).length = 66. Moving resource.
Dummy(const &Dummy).length = 55.Copying resource.
Dummy(const &Dummy).length = 77.Copying resource.
~Dummy().length = 55.
delete resource.
~Dummy().length = 77.
delete resource.
~Dummy().length = 0.
~Dummy().length = 55.
delete resource.
~Dummy().length = 66.
delete resource.
~Dummy().length = 77.
delete resource.

4. 移动构造函数

  下面是一个最简单的移动构造函数:

Dummy(Dummy&& other) noexcept // C++11 - specify non-exception throwing function{  _date = other._data;  // shallow copy  other._date = nullptr;}

  注意:上面函数没有分配任何新的资源,只是将内容移动了而不是拷贝: other中的内容移到了一个新成员里边,然后other中的内容被清除了。它占用了other的资源并且将other设置为了默认构造函数时的状态。最重要的点是没有内容的分配开销。我们只是分配了一个地址,只需很少的几个机器指令即可实现。

  假设这个地址指向的是包含上万个整数的数组,我们无需拷贝其中的元素,无需创建新的东西,而只是移动了它们。如果使用旧的拷贝构造函数,且这个类有一个拥有上万个元素的成员数组,则我们需要大量的赋值操作,代价很大。现在,有了移动构造函数之后,可以节省很多。
  移动构造函数比拷贝构造函数快很多,因为它既不分配内存,也不拷贝内存块。

5. 移动赋值运算符

  一个简单的移动赋值运算符如下:

Dummy& operator=(Dummy&& other) noexcept{  _data =  other._data;  other._data = nullptr;  return *this;}

  移动赋值运算符与拷贝构造函数类似,除了转移源object之前,它会释放object所拥有的资源。步骤如下:

  1). 释放*this所拥有的资源
  2). 转移other的资源
  3). 将other设置为默认状态
  4). 返回*this
  5). 结果分析
  因为C++11支持右值引用,这样std::vector::push_back()函数相当于有两个版本:一个像以前一样接受左值参数const T&, 另外一个新的接受右值参数T&&。
 main()函数中调用了两次push_back来插入到vector中:

std::vector vec;vec.push_back(A(55));vec.push_back(A(77));

  这两个push_back,实际上都使用的push_back(T&&), 因为传入的参数是右值。push_back(T&&)会将参数中的资源,移动到vector中的对象A, 使用的是A的移动构造函数。在C++03中,相同的这段代码会进行参数的赋值,因为会调用参数的拷贝构造函数。

  如果传入的参数是一个左值,则push_back(const T&)会被调用:

std::vector vec;A obj(25);          //lvaluevec.push_back(obj); //push_back(const T&)

  不过,我们可以使用static_cast将左值引用转换为右值引用,使得调用的是push_back(T&&)。

// calls push_back(T&&)vec.push_back(static_cast
(obj));

  另外一种办法是,使用std::move()来实现:

// calls push_back(T&&)vec.push_back(std::move(obj));

  总结起来,push_back(T&&)看似总是最优选择,因为它减少了不必要的拷贝开销。

  然而,需要注意的是push_back(T&&)总会清空传入的参数。如果需要在执行一个push_back()操作后,仍然保持参数原始状态,则还是需要选择拷贝语义(拷贝构造函数),而不是移动语义。

6. 使用move()交换对象

  下面例子显示如何使用move来交互对象

#include
class Dummy{public: //constructor explicit Dummy(size_t length):_length(length),_data(new int[length]) { } //move constructor Dummy(Dummy&& other) { _data = other._data; _length = other._length; other._data = nullptr; other._length = 0; } //move assignment operator Dummy& operator= (Dummy&& other) noexcept { _data = other._data; _length = other._length; other._data = nullptr; other._length = 0; return *this; } void swap(Dummy& other) { Dummy tmp = std::move(other); other = std::move(*this); *this = std::move(tmp); } int getLength() { return _length; } int* getData() { return _data; }private: size_t _length; int* _data; };int main(){ Dummy a(11),b(22); std::cout<
<<" "<
<

  运行结果为:

  11 22
  0x1a31010 0x1a31050
  22 11
  0x1a31050 0x1a31010

转载地址:http://umeji.baihongyu.com/

你可能感兴趣的文章
linux内核学习(7)脱胎换骨解压缩的内核
查看>>
慢慢欣赏linux 内核模块引用
查看>>
kprobe学习
查看>>
慢慢欣赏linux CPU占用率学习
查看>>
Homebrew指令集
查看>>
React Native(一):搭建开发环境、出Hello World
查看>>
React Native(二):属性、状态
查看>>
JSX使用总结
查看>>
React Native(四):布局(使用Flexbox)
查看>>
React Native(七):Android双击Back键退出应用
查看>>
Android自定义apk名称、版本号自增
查看>>
adb command not found
查看>>
Xcode 启动页面禁用和显示
查看>>
【剑指offer】q50:树中结点的最近祖先
查看>>
二叉树的非递归遍历
查看>>
【leetcode】Reorder List (python)
查看>>
【leetcode】Linked List Cycle (python)
查看>>
【leetcode】Candy(python)
查看>>
【leetcode】Sum Root to leaf Numbers
查看>>
【leetcode】Pascal's Triangle II (python)
查看>>