前言
繼續閱讀之前,你最好了解了左值,右值,左值引用,右值引用等概念。
最好閱讀了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在運行時不做任何事情。