C++11 move帶來的高效

前言

繼續閱讀之前,你最好了解了左值,右值,左值引用,右值引用等概念

引入

我由淺入深的引入move,先來看一個例子:
自己實現一個string類CMyString(簡單實現了幾個需要的函數),相信很多c++程序員面試的時候都會遇到,代碼如下
//

#include "stdafx.h"
#include <iostream>
using namespace std;

class CMyString
{
public:
	CMyString(char* pStr)
		: m_pStr(NULL)
		, m_nLen(0)
	{
		if (NULL != pStr)
		{
			m_nLen = strlen(pStr);
			m_pStr = new char[m_nLen + 1];
			memcpy(m_pStr, pStr, m_nLen);
			m_pStr[m_nLen] = 0;
			cout << "一般構造函數 str=" << m_pStr << endl;
		}		
	}
	
	CMyString(const CMyString& o)
		: m_pStr(NULL)
		, m_nLen(0)
	{
		if (NULL != o.m_pStr)
		{
			m_nLen = o.m_nLen;
			m_pStr = new char[m_nLen + 1];
			memcpy(m_pStr, o.m_pStr, m_nLen);
			m_pStr[m_nLen] = 0;
			cout << "拷貝構造函數 str=" << m_pStr << endl;
		}		
	}

	const CMyString& operator=(const CMyString& o)
	{
		if (this != &o)
		{
			if (NULL != m_pStr)
			{
				delete[] m_pStr;
				m_pStr = NULL;
			}
			m_nLen = o.m_nLen;
			if (NULL != o.m_pStr)
			{
				m_pStr = new char[m_nLen + 1];
				memcpy(m_pStr, o.m_pStr, m_nLen);
				m_pStr[m_nLen] = 0;
			}			
			cout << "重載賦值運算符 str=" << m_pStr << endl;
		}
		return *this;
	}	

	~CMyString()
	{
		if (NULL != m_pStr)
		{
			//cout << "析構函數 str=" << m_pStr << endl;
			delete m_pStr;			
		}		
	}

	char* GetData()
	{
		return m_pStr;
	}	

private:
	char* m_pStr;
	int m_nLen;
};

void swap(CMyString& str1, CMyString& str2)
{
	cout << "********************************************" << endl;
	CMyString tmp = str1;
	str1 = str2;
	str2 = tmp;
	cout << "********************************************" << endl;
}

int _tmain(int argc, _TCHAR* argv[])
{
	CMyString str1("hello this is str1");
	CMyString str2("hello this is str2");

	swap(str1, str2);

	cout << "str1.GetData:" << str1.GetData() << endl;
	cout << "str2.GetData:" << str2.GetData() << endl;
	system("pause");
}

上面代碼中寫了一個一般的swap函數(需要構建臨時對象,一次拷貝構造,兩次賦值)來交換兩個CMyString的值,運行一下看看執行情況


是的,swap裏面調用了一次拷貝構造,兩次賦值操作,成功交換了兩個對象的值,這個過程執行了很多new memcpy delete操作,在string內容很多的情況下,效率可想而知,我們都知道,swap裏面是可以優化的,可以不創建臨時對象,爲此我們給CMyString函數增加一個swap成員函數來實現交換
void swap(CMyString& o)
	{
		char* pStrTmp = o.m_pStr;
		int nLen = o.m_nLen;

		o.m_pStr = m_pStr;
		o.m_nLen = m_nLen;

		m_pStr = pStrTmp;
		m_nLen = nLen;		
	}
交換的時候使用
str1.swap(str2);
看一下執行結果


通過直接交換內部數據指針的方式,成功交換了兩個值,省去了很多無意義的new delete操作(實際std::string::swap函數實現原理類似)

進階

再來看一個例子,假如有這樣一種場景:我們構建一個CMyString對象str,使用它執行了一些操作,然後通過一個函數將str賦值爲另一個值(這是一種很常見的場景)
CMyString GetCMyString()
{
	return CMyString("hello this is the other one");
}

int _tmain(int argc, _TCHAR* argv[])
{
	CMyString str("this is str");
	// use str do something
	// fun(str);

	str	= GetCMyString();
	system("pause");
        return 0;
}
如上代碼我們構建str並在fun中執行一系列操作之後使用GetCMyString給str賦予了新的值,執行效果如下:


如圖所示,構造了other one,然後重載賦值運算符給str賦予了新的值,這裏我們可以思考,能不能像上一個例子一樣,通過交換指針的方式,優化掉重載賦值運算符函數中
delete new操作,因爲other one是一個臨時變量,return 之後立馬就銷燬了,沒有其他的作用,所以我可以將str和它交換指針,這樣str有了新值,other one析構銷燬了原先str中的內容,也就是說因爲臨時對象馬上就要銷燬了,所以我們可以只是使用指針的交換來實現了構造的效果你可能會這樣做,直接將重載賦值運算符函數改爲如下:
	const CMyString& operator=(CMyString& o)
	{
		char* pStrTmp = o.m_pStr;
		int nLen = o.m_nLen;

		o.m_pStr = m_pStr;
		o.m_nLen = m_nLen;

		m_pStr = pStrTmp;
		m_nLen = nLen;

		return *this;
	}		
執行效果如下:

雖然優化掉了重載賦值運算符函數中delete new操作,而且在本例中運行正常,但顯然這是不可行的,本例中other one是一個臨時變量,被返回後立馬就銷燬了。
但是如果是如下代碼,肯定就出問題了
CMyString str("this is str");
	CMyString strImp("this is strimp");
	str = strImp;
	cout << "strImp.GetData:" << strImp.GetData() << endl;
	system("pause");
我本想將strImp賦值給str,但卻修改了strImp中的內容,這不是我們期望的結果,那有沒有這樣一種方式:
如果是other one這種臨時變量,我用交換指針的方式實現重載賦值運算符函數
如果是strImp這種長生命週期的普通變量,我執行原來的方式實現重載賦值運算符函數

c++11之後有了右值引用,我們可以實現這個思路,我們可以發現other one 是右值,strImp是左值,我們根據這個特性,可以增加一個參數爲右值引用的重載賦值運算符函數,
如下所示:
const CMyString& operator=(CMyString&& o)
	{
		char* pStrTmp = o.m_pStr;
		int nLen = o.m_nLen;

		o.m_pStr = m_pStr;
		o.m_nLen = m_nLen;

		m_pStr = pStrTmp;
		m_nLen = nLen;	
		cout << "右值引用類型 重載賦值運算符 str=" << m_pStr << endl;
		return *this;
	}
測試一下:
int _tmain(int argc, _TCHAR* argv[])
{
	CMyString str("this is str");
	CMyString strImp("this is strimp");	

	// use str do something
	// fun(str);

	str	= GetCMyString();
	cout << "str.GetData:" << str.GetData() << endl;

	str = strImp;
	cout << "strImp.GetData:" << strImp.GetData() << endl;	
	system("pause");
	return 0;
}


other one賦值調用的是右值引用類型的重載賦值運算符函數
strImp賦值調用的是普通的重載賦值運算符函數

有了右值引用類型 重載賦值運算符函數,同理可以引出右值引用類型 拷貝構造函數,實現如下:
CMyString(CMyString&& o)
		: m_pStr(NULL)
		, m_nLen(0)
	{
		char* pStrTmp = o.m_pStr;
		int nLen = o.m_nLen;

		o.m_pStr = m_pStr;
		o.m_nLen = m_nLen;

		m_pStr = pStrTmp;
		m_nLen = nLen;	
		cout << "右值引用類型 拷貝構造函數 str=" << m_pStr << endl;		
	}

move

此時我們回顧一開始我們實現的普通swap函數和swap成員函數
void swap(CMyString& str1, CMyString& str2)
{
	cout << "********************************************" << endl;	
	CMyString tmp = str1;
	str1 = str2;
	str2 = tmp;
	cout << "********************************************" << endl;
}
void swap(CMyString& o)
	{
		char* pStrTmp = o.m_pStr;
		int nLen = o.m_nLen;

		o.m_pStr = m_pStr;
		o.m_nLen = m_nLen;

		m_pStr = pStrTmp;
		m_nLen = nLen;		
	}
之前我們發現,普通swap函數中的tmp對象帶來了多餘的new delete操作,我們使用swap成員函數優化掉了,如果不用swap成員函數來進行優化,有辦法嗎?
分析一下,普通swap函數中的三行代碼執行了哪三個函數?  一次拷貝構造,兩次賦值
結合進階中的右值引用類型 重載賦值運算符函數和右值引用類型 拷貝構造函數來分析:
只要在執行拷貝構造函數的時候指定去執行右值引用類型 拷貝構造函數
執行賦值的時候去執行右值引用類型 重載賦值運算符函數,就可以實現,但是右值引用類型重載賦值運算符函數和右值引用類型 拷貝構造函數只有在
參數是右值的時候纔會被調用,而swap函數中str1, tmp, str2都是左值

有沒有辦法把等號右側的參數轉換它們爲右值呢?move出場了
move的作用:它接受一個參數,然後返回一個該參數對應的右值引用
swap改造如下:
void swap(CMyString& str1, CMyString& str2)
{
	cout << "********************************************" << endl;	
	CMyString tmp = std::move(str1);
	str1 = std::move(str2);
	str2 = std::move(tmp);
	cout << "********************************************" << endl;
}
測試一下:
int _tmain(int argc, _TCHAR* argv[])
{
	CMyString str1("hello this is str1");
	CMyString str2("hello this is str2");
	swap(str1, str2);
	cout << "str1.GetData" << str1.GetData() << endl;
	cout << "str2.GetData" << str2.GetData() << endl;
	system("pause");
}


總結

我們實現自己的類的時候,要有意識的實現右值引用類型的拷貝構造函數和右值引用類型的重載賦值運算符函數,
這樣在交換的就可以使用move語義實現高效交換了(stl自帶的類已經實現了)。









發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章