C++11线程指南(4)--右值引用与移动语义

目录

1. 按值传递

2. 右值引用

3. 移动语义

4. 移动构造函数

5. 移动赋值运算符

6. 使用move()交换对象


1. 按值传递

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

2. 右值引用

  此处介绍右值引用的目的,是为了实现后面的移动语义。
  右值引用可以帮助我们分辨一个值是左值还是右值。C++11标准规定右值引用只能绑定右值,它使用两个&符号进行声明。

int&& rvalue_ref = 99;

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

#include<iostream>

void f(int& i) {
	std::cout<<"lvalue ref: "<<i<<std::endl;
}
void f(int&& i) {
	std::cout<<"rvalue ref: "<<i<<std::endl;
}

int main()
{
	int i=99;
	f(i);    //lvalue ref called
	f(99);   //rvalue ref called

	f(std::move(i));  //rvalue ref called

	return 0;
}

  运行结果为:
  lvalue ref: 99
  rvalue ref: 99
  rvalue ref: 99

  其中std::move函数的作用就是将一个左值转换为成右值。

  如果一个表达式会生成一个临时对象,则它就是一个右值。如下:

#include<iostream>

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

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

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

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

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

const int&& val = getValue(); //OK
int&& 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<iostream>

void printRef(int& value) {
	std::cout<<"lvalue: value = "<<value<<std::endl;
}
void printRef(int&& value) {
	std::cout<<"rvalue: value = "<<value<<std::endl;
}
int getVal() {
	int tmp = 88;
	return tmp;
}
int main(){
	int i = 11;
	printRef(i);
	printRef(getVal());  //printRef(88);
	return 0;
}

  运行结果为:
  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<iostream>
#include<algorithm>
#include<vector>

class Dummy {
public:
	explicit Dummy(size_t length):_length(length), _data(new int[length]) {
		std::cout<<"Dummy(size_t).length = "<<_length<<"."<<std::endl;
	}
	~Dummy() {
		std::cout<<"~Dummy().length = "<<_length<<"."<<std::endl;
		if(_data!=NULL){
			std::cout<<"delete resource."<<std::endl;
			delete _data;
		}
	}
	Dummy(const Dummy&other):_length(other._length),_data(new int[other._length]) {
		std::cout<<"Dummy(const &Dummy).length = "<<other._length<<".Copying resource."<<std::endl;
		std::copy(other._data, other._data+_length, _data);
	}
	//赋值运算符
	Dummy& operator=(const Dummy& other) {
		std::cout<<"operator=(const Dummy&).length = "<<other._length<<". Copying resource."<<std::endl;
		if(this != &other) {
			delete _data;
			_length = other._length;
			_data = new int[_length];
			std::copy(other._data, other._data+_length, _data);
		}
		return *this;
	}
	//移动语义的构造函数
	Dummy(Dummy&& other):_length(0),_data(NULL) {
		std::cout<<"Dummy(Dummy&&).length = "<<other._length<<". Moving resource."<<std::endl;
		//从源对象复制数据指针以及数据长度
		_data = other._data;
		_length = other._length;
		//修改源对象中的数据指针,这样析构的时候不会多次释放内存。
		other._data = NULL;
		other._length = 0;
	}
	//移动语义的赋值运算符
	Dummy& operator=(Dummy&& other) {
		std::cout<<"operator=(Dummy&&).length = "<<other._length<<"."<<std::endl;
		if(this != &other) {
			delete _data;
			//从源对象复制数据指针以及数据长度
			_data = other._data;
			_length = other._length;
			//修改源对象中的数据指针,这样析构的时候不会多次释放内存。
			other._data = NULL;
			other._length = 0;
		}
		return *this;
	}
private:
	size_t _length;
	int* _data;
};

int main() {
	std::vector<Dummy> 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<A> 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<A> vec;
A obj(25);          //lvalue
vec.push_back(obj); //push_back(const T&)

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

// calls push_back(T&&)
vec.push_back(static_cast<A&&>(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<iostream>

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<<a.getLength()<<" "<<b.getLength()<<std::endl;
	std::cout<<a.getData()<<" "<<b.getData()<<std::endl;
	a.swap(b);
	std::cout<<a.getLength()<<" "<<b.getLength()<<std::endl;
	std::cout<<a.getData()<<" "<<b.getData()<<std::endl;
	return 0;
}

  运行结果为:
  11 22
  0x1a31010 0x1a31050
  22 11
  0x1a31050 0x1a31010

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值