C++11 forward完美轉發

前言

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

最好閱讀了C++11 move帶來的高效

引入

這裏我借上一篇C++11 move帶來的高效中的CMyString類用一下,代碼如下

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=(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;
	}
	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;
	}	
	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;		
	}
	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;		
	}
private:
	char* m_pStr;
	int m_nLen;
};
舉一個栗子:我們內部有一個CoreFun函數需要一個CMyString對象,CoreFun函數我們不想暴露給客戶,所以我們再封裝一個ICoreFun來調用CoreFun,代碼如下:

void CoreFun(CMyString t) 
{
	cout << "CoreFun" << endl;
}

void ICoreFun(CMyString t) 
{
	cout << "ICoreFun" << endl;
	CoreFun(t);

很顯然,ICoreFun函數只是起了一個轉發的作用

測試一下:

int _tmain(int argc, _TCHAR* argv[])
{	
	CMyString lvalue("hello this is the lvalue");
	ICoreFun(lvalue);
	system("pause");
}

執行效果如下:

很顯然,中間的一次拷貝構造函數是多餘的,我們可以通過引用來優化掉:

void ICoreFun(CMyString& t) 
{
	cout << "ICoreFun" << endl;
	CoreFun(t);
}


進階

上面我們的ICoreFun函數的參數是一個左值引用,如果我需要傳遞一個右值進來的時候怎麼辦呢?例如ICoreFun(CMyString("hello this is the rvalue"));

(注:VS上自定義類對象右值也可以綁定到左值引用上)

難道要再寫一個ICoreFun(CMyString&&)的接口嗎?有沒有什麼辦法

當我ICoreFun(lvalue);的時候ICoreFun的函數原型是ICoreFun(CMyString&)

當我ICoreFun(CMyString("hello this is the rvalue"));的時候ICoreFun的函數原型是ICoreFun(CMyString&&)

模板是一個很好的選擇,如下的模板可以解決這個問題:

template <typename T>
void CoreFun(T t)
{
	cout << "CoreFun" << endl;
}

template <typename T>
void ICoreFun(T&& t)
{
	cout << "ICoreFun" << endl;
	CoreFun(t);
}
想要理解如上模板的函數中T&&如何實現我們上面的需求的,需要知道C11引入的 “引用摺疊規則”reference collapsing rules),有如下兩條

對右值引用的兩個規則中的第一個也同樣影響了舊式的左值引用。回想在pre-11 C++時,對一個引用取引用是是不被允許的,比如A& &會導致一個編譯器錯誤。相比之下,在C++11中,引入瞭如下所示的引用摺疊規則(reference collapsing rules):

1. A& &變成A&
2. A& &&變成A&
3. A&& &變成A&
4. A&& &&變成A&&
第二條,有一個對應於函數模板中模板參數的特殊的演繹規則,當其模板參數爲右值引用類型的時候:

template<typename T>
void foo(T&&);
下面,這些規則會生效:

當foo被一個類型爲A的左值調用時,T會被轉化爲A&,因此根據上面的引用摺疊規則,這個參數類型實際上會變成A&。
當foo被一個類型爲A的右值調用是,T會被轉化成A,因此這個參數類型實際上會變成A&&。

現在我們根據上面的規則來推到一下,T&&是怎麼實現

當我ICoreFun(lvalue);的時候ICoreFun的函數原型是ICoreFun(CMyString&)

當我ICoreFun(CMyString("hello this is the rvalue"));的時候ICoreFun的函數原型是ICoreFun(CMyString&&)的:


運行程序看一下效果:

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

	ICoreFun(lvalue);
	cout << endl;
	ICoreFun(CMyString("hello this is the rvalue"));

	system("pause");
}


這裏可以思考了,ICoreFun參數是右值的時候,拷貝構造函數是可以用 右值類型的拷貝構造函數優化的,回顧上一篇move帶來的高效中,我們掌握了一種優化方式:對於使用臨時對象來構造另一個對象的時候,完全可以通過右值類型的拷貝構造函數中的高效交換來實現,而不是調用拷貝構造函數重新進行new delete操作,因爲臨時對象馬上就要銷燬了,所以我們可以只是使用指針的交換來實現了構造的效果

但是雖然ICoreFun(CMyString("hello this is the rvalue")); 轉換爲了ICoreFun(CMyString&& t),但是,t是一個左值,所以CoreFun(t);的時候調用的是拷貝構造函數來構造t,

t是一個左值都能理解吧,就和

int&& r = 8;

8是一個右值,r的類型是右值引用,r本身是一個左值,是一個道理的

全都是因爲增加了一層轉發,CMyString("hello this is the rvalue")從右值變爲了左值

forward

有沒有辦法,在上面的情況中,在CMyString&& t 傳遞給CoreFun(t)的時候,CoreFun(t)把t當作左值來處理呢?也就在t是CMyString&&類型的時候把t轉換爲右值,在t是CMyString& t類型的時候不轉換。forward就是這個作用。

forward作用:獲取參數的原有類型(不太形象,具體看總結中的定義去理解下)

代碼修改如下:

template <typename T>
void CoreFun(T t)
{
	cout << "CoreFun" << endl;
}

template <typename T>
void ICoreFun(T&& t)
{
	cout << "ICoreFun" << endl;
	CoreFun(std::forward<T>(t));
}
運行結果:


轉發左值就調用拷貝構造函數,抓發右值就調用右值類型的拷貝構造函數,完美。

總結

std::forward argument: Returns an rvalue reference to arg if arg is not an lvalue reference; If arg is an lvalue reference, the function returns arg without modifying its type.

std::forward:This is a helper function to allow perfect forwarding of arguments taken as rvalue references to deduced types, preserving any potential move semantics involved. 

std::forward<T>(u)有兩個參數:T 與 u。當T爲左值引用類型時,u將被轉換爲T類型的左值,否則u將被轉換爲T類型右值。如此定義std::forward是爲了在使用右值引用參數的函數模板中解決參數的完美轉發問題。

std::move是無條件的轉爲右值引用,而std::forward是有條件的轉爲右值引用,更準確的說叫做Perfect forwarding(完美轉發),而std::forward裏面蘊含着的條件則是Reference Collapsing(引用摺疊)。

std::move不move任何東西。std::forward也不轉發任何東西。在運行時,他們什麼都不做。不產生可執行代碼,一個比特的代碼也不產生。

std::move和std::forward只是執行轉換的函數(確切的說應該是函數模板)。std::move無條件的將它的參數轉換成一個右值,而std::forward當特定的條件滿足時,纔會執行它的轉換。

std::move表現爲無條件的右值轉換,就其本身而已,它不會移動任何東西。 std::forward僅當參數被右值綁定時,纔會把參數轉換爲右值。 std::move和std::forward在運行時不做任何事情。



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