C/C++C++11新特性:初探右值引用与转移语义

Posted mick_seu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C/C++C++11新特性:初探右值引用与转移语义相关的知识,希望对你有一定的参考价值。

参考自:右值引用与转移语义(李胜利)


C++11之前,右值是不能被引用的,最大限度就是用常量引用绑定一个右值,如 :

const int& a = 1;

为了与左值引用区分,右值引用 && 表示。如下:

#include <iostream>

void fun(int& i) 
	std::cout << "lvalue:" << i << std::endl;


void fun(int&& i) 
	std::cout << "rvalue:" << i << std::endl;


int main() 
	int a = 0;
	fun(a);
	fun(1);
	return 0;


输出:

lvalue:0
rvalue:1
可以发现,临时变量 1 使用了入参为右值引用的 fun 函数完成了函数调用。


右值引用的出现解决了C++11之前移动对象效率问题。下面用自定义的String类来初识转移语义。首先定义一个不带转移语义的普通类:

#include <iostream>
#include <vector>
#include <string.h>

class String 
	public:
		String() 
			std::cout << "String()" << std::endl;
		;
		String(const char* str) 
			len_ = strlen(str);
			Init(str);
			std::cout << "String(const char*)" << std::endl;
		
		String(const String& str) 
			len_ = str.len_;
			Init(str.data_);
			std::cout << "String(const String&)" << std::endl;
		
		String& operator= (const String& str) 
			if (this != &str) 
				len_ = str.len_;
				delete data_;
				Init(str.data_);
			
			std::cout << "operator=" << std::endl;
		
		~String() 
			if (data_)  delete data_; 
			std::cout << "~String()" << std::endl;
		

	private:
		void Init(const char* str) 
			data_ = new char[len_ + 1];
			memcpy(data_, str, len_);
			data_[len_] = '\\0';
		
		char* data_ = nullptr;
		uint32_t len_ = 0;
;

int main() 
	String a;
	a = String("hello");
	std::vector<String> vec;
	vec.push_back("world");

	return 0;

输出:

String()
String(const char*)	// 1
operator=		// 2
~String()
String(const char*)	// 3
String(const String&)	// 4
~String()
~String()
~String()

上述代码中,String(“hello”) 和 String(“world”) 都是临时对象,也就是右值。整个过程中,我们实际只需要2个对象,即只需要2次内存分配即可,实际确是4次,造成了没有意义的资源申请和释放的操作。如果能够直接使用临时对象已经申请的资源,既能节省资源,有能节省资源申请和释放的时间。这正是定义转移语义的目的。


巧的是:C++11后 vector提供了 emplace_back() 可以就地构造对象,从而减少一次拷贝构造。不过对于 a = String("hello") 我们还得借助转移语义。


下面给我们为 String 添加移动构造函数及移动拷贝赋值函数。

#include <iostream>
#include <vector>
#include <string.h>

class String 
	public:
		String() 
			std::cout << "String()" << std::endl;
		;
		String(const char* str) 
			len_ = strlen(str);
			Init(str);
			std::cout << "String(const char*)" << std::endl;
		
		String(const String& str) 
			len_ = str.len_;
			Init(str.data_);
			std::cout << "String(const String&)" << std::endl;
		
		String(String&& str) 						// 移动构造函数
			len_ = str.len_;
			data_ = str.data_;
			str.len_ = 0;
			str.data_ = nullptr;
			std::cout << "move String(const String&&)" << std::endl;
		
		String& operator= (const String& str) 
			if (this != &str) 
				len_ = str.len_;
				delete data_;
				Init(str.data_);
			
			std::cout << "operator=" << std::endl;
		
		String& operator= (String&& str) 				// 移动赋值函数
			if (this != &str) 
				len_ = str.len_;
				delete data_;
				data_ = str.data_;
				str.len_ = 0;
				str.data_ = nullptr;
			
			std::cout << "move operator=" << std::endl;
		
		~String() 
			if (data_)  delete data_; 
			std::cout << "~String()" << std::endl;
		

	private:
		void Init(const char* str) 
			data_ = new char[len_ + 1];
			memcpy(data_, str, len_);
			data_[len_] = '\\0';
		
		char* data_ = nullptr;
		uint32_t len_ = 0;
;

int main() 
	String a;
	a = String("hello");
	std::vector<String> vec;
	vec.push_back("world");

	return 0;
输出如下:

String()
String(const char*)		// 1
move operatro=
~String()
String(const char*)		// 2
move String(const String&&)
~String()
~String()
~String()
原先的拷贝构造函数和拷贝赋值函数的调用被移动函数替代,减少了两次构造过程,节省了资源,提高了程序运行的效率。


几个注意点

1. 参数(右值)的符号必须是右值引用符号,即“&&”。
2. 参数(右值)不可以是常量,因为我们需要修改右值。
3. 参数(右值)的资源链接和标记必须修改。否则, 右值的析构函数就会释放资源。转移到新对象的资源也就无效了。




标准库函数 std::move

编译器只对右值引用才能调用转移构造函数和转移赋值函数,而所有命名对象都只能是左值引用,那么如何对左值使用移动函数,即把一个左值引用当做右值引用来使用,怎么做呢?标准库提供了函数 std::move,这个函数以非常简单的方式将左值引用转换为右值引用。

#include <iostream>

void fun(int& i) 
	std::cout << "lvalue:" << i << std::endl;


void fun(int&& i) 
	std::cout << "rvalue:" << i << std::endl;


int main() 
	int a = 0;
	fun(std::move(a));
	fun(1);
	return 0;
输出如下:

rvalue:0
rvalue:1

std::move在提高 swap 函数的的性能上非常有帮助,一般来说,swap函数的通用定义如下:

template <classT> swap(T& a, T& b) 

       T tmp(a);   // copy a to tmp 
       a = b;      // copy b to a 
       b = tmp;    // copy tmp to b 
有了 std::move,swap 函数的定义变为 :

template <classT> swap(T& a, T& b) 

       T tmp(std::move(a)); // move a to tmp 
       a = std::move(b);    // move b to a 
       b = std::move(tmp);  // move tmp to b 


通过 std::move,一个简单的 swap 函数就避免了 3 次不必要的拷贝操作。

以上是关于C/C++C++11新特性:初探右值引用与转移语义的主要内容,如果未能解决你的问题,请参考以下文章

虚幻4与现代C++:转移语义和右值引用

C++基础(插入1)——C++11新特性:右值引用移动语义完美转发

C++11新特性详解

C++11新特性详解

C++11新特性详解

右值引用&&