std:forward 完美转发
概述:
?? ?// TEMPLATE CLASS identity
template<class _Ty>
?? ?struct identity
?? ?{?? ?// map _Ty to type unchanged
?? ?typedef _Ty type;
?? ?const _Ty& operator()(const _Ty& _Left) const
?? ??? ?{?? ?// apply identity operator to operand
?? ??? ?return (_Left);
?? ??? ?}
?? ?};
?? ?// TEMPLATE FUNCTION forward
template<class _Ty> inline
?? ?_Ty&& forward(typename identity<_Ty>::type& _Arg)
?? ?{?? ?// forward _Arg, given explicitly specified type parameter
?? ?return ((_Ty&&)_Arg);
?? ?}
?? ??? ? 調用方式: ???? _STD forward<_Ty>(_Val));
簡單來說,identity 用于告訴編譯器,我不要你幫我推導類型了(寫成式子就是 forward(_val), 不帶 <_Ty> 的 ),我自己明確指定類型 ( identity<_Ty>::type)。這樣在傳入參數時, 編譯器就不會自己猜想:把 _arg 想成左值了(折疊規則,這個規則是寫這個編譯器的人規定的,我們就不要費力去問為什么要這樣做了,如果你偏要問,那你可以自己定義規則,這樣就變成了你自己的編譯器了)。
X& + & => X&????? (1)
X&& + & => X&?? (2)
X& + && => X&?? (3)
X&& + && => X&& (4)
outer(X&& t)
{
? inner(t)
}
inner(X&&)
給它變成
? inner( _STD forward<_Ty>(t))
那么這時編譯器執行函數 inner前 , 會對傳給 t 的類型 進行推導, 而當指明類型為? identity<_Ty>::type, 編譯器放棄了自動推導,認為調用者已經明確指定了類型為 ...type, 當然 forward 返回的類型也要為 右值 X&&。你也許會問為什么要為 X&&, 而不為 X&. 這不廢話嗎,舊的編譯器由于推導機制會把 右值認為成左值,不會把左值認為是右值,使用新的 forward ,就是為了保持 右值的性質不變。
上篇:
轉發問題
在程序員不用寫高度泛化的代碼的時候,C++98/03 的 lvalue, rvalue, 引用,還有模板看起來是很完美的。假設你要寫一個完全泛化的函數 outer(),這個函數的目的是將任意數目個任意類型的參數傳遞(也就是“轉發”)給函數 inner()。已有很多不錯的解決方案,比如 factory 函數 make_shared<T>(args) 是把 args 傳給 T 的構造函數,然后返回 shared_ptr<T>。(這樣就把 T 對象和用于對它進行引用計數的代碼存儲到同一塊動態內存中,性能上與侵入式引用計數一樣好); 而像 function<Ret(args)> 這樣的包裝類是把參數傳給其內部存儲的函數對象(functor),等等。在這篇文章里,我們只對 outer() 是如何把參數傳遞給 inner() 這部分感興趣。至于 outer() 的返回類型是怎么決定的是另外的問題(有時候很簡單,如 make_shared<T>(args) 總是返回 shared_prt<T>,),但要在完全搞定這個問題的一般化情況,你就要用到 C++0x的 decltype 特性了)。
如果不帶參數,就不存在這樣的問題,那么帶一個參數情況呢?讓我們嘗試寫個 outer() :
template <typename T> void outer(T& t) {
??? inner(t);
}
問 題來了,如果傳給它的參數是非常量 rvalue,那我們就無法調用 outer()。如果 inner() 接收 const int& 型的參數,那 inner(5) 是可以通過編譯的,但是 outer(5) 就編譯不過了。因為 T 會被推導為 int, 而 int& 是不能綁定到常量 5 的。
好吧,讓我們試試這個:
template <typename T> void outer(const T& t) {
??? inner(t);
}
如果 inner()接收 int& 型參數,那就會違法 const 正確性,編譯都過不了。
現在,你可以重載兩個分別帶 T& 和 const T& 參數的 outer(),這確實管用。當你調用 outer()時,就像直接調用 inner() 一樣。
可惜的是,這中方法在多參數的情況下就麻煩了(譯注:要寫的重載函數太多了)。你就得為每一個參數像 T1& 和 const T1&, T2& 和 const T2& 等這樣進行重載,要重載的函數數目呈指數級增長。(VC9 SP1 的 tr1::bind() 就夠讓人感到絕望了,它為 5 個參數這么重載出了 63 個函數。如果不這么蠻干的話,沒有像這里的長篇累述,我們就很難跟使用者解釋為什么不能調用用 1729 這樣的 ravlue 做參數的函數。為了產生出這些重載函數使用了令人作嘔的預處理機制,惡心到你都不想知道它)。
在 C++98/03 中,轉發問題是很嚴重的,而且本質上無解(必須求助于惡心的預處理機制,這會嚴重拖慢編譯速度,還讓代碼變得難以閱讀)。總算, rvalue 優雅地解決了這個問題。
完美轉發: 模式
完美轉發讓你能簡單而清晰地只寫一個模板函數就可以轉發所有的參數給任意函數,不管它帶幾個參數,也不管參數類型是什么。而且參數的非常量/常量, lvalue/rvalue 屬性都能得以保留,讓你可以像使用 inner() 一樣使用 outer(),還可以和 move 語意一起用從而獲得額外的好處。( C++0x 的變長模板技術解決了“任意數目”這部分,我們在這里把 N 看做任意數目)。乍看之下很神奇,實際上很簡單:
C:\Temp>type perfect.cpp
#include <iostream>
#include <ostream>
using namespace std;
template <typename T> struct Identity {
??? typedef T type;
};
template <typename T> T&& Forward(typename Identity<T>::type&& t) {
??? return t;
}
void inner(int&, int&) {
??? cout << "inner(int&, int&)" << endl;
}
void inner(int&, const int&) {
??? cout << "inner(int&, const int&)" << endl;
}
void inner(const int&, int&) {
? ??cout << "inner(const int&, int&)" << endl;
}
void inner(const int&, const int&) {
??? cout << "inner(const int&, const int&)" << endl;
}
template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) {
??? inner(Forward<T1>(t1), Forward<T2>(t2));
}
int main() {
??? int a = 1;
??? const int b = 2;
??? cout << "Directly calling inner()." << endl;
??? inner(a, a);
??? inner(b, b);
??? inner(3, 3);
??? inner(a, b);
??? inner(b, a);
??? inner(a, 3);
??? inner(3, a);
??? inner(b, 3);
??? inner(3, b);
??? cout << endl << "Calling outer()." << endl;
??? outer(a, a);
??? outer(b, b);
??? outer(3, 3);
??? outer(a, b);
??? outer(b, a);
??? outer(a, 3);
??? outer(3, a);
??? outer(b, 3);
??? outer(3, b);
}
C:\Temp>cl /EHsc /nologo /W4 perfect.cpp
perfect.cpp
C:\Temp>perfect
Directly calling inner().
inner(int&, int&)
inner(const int&, const int&)
inner(const int&, const int&)
inner(int&, const int&)
inner(const int&, int&)
inner(int&, const int&)
inner(const int&, int&)
inner(const int&, const int&)
inner(const int&, const int&)
Calling outer().
inner(int&, int&)
inner(const int&, const int&)
inner(const int&, const int&)
inner(int&, const int&)
inner(const int&, int&)
inner(int&, const int&)
inner(const int&, int&)
inner(const int&, const int&)
inner(const int&, const int&)
兩行!完美轉發只用了兩行!夠簡潔吧!
這個例子示范了怎么把 t1 和 t2 從 outer() 透明地轉發給 inner(); inner() 可以知道它們的非常量/常量, lvalue/ravlue 屬性,就像inner是被直接調用的那樣。
跟 std::move() 一樣, std::identify 和 std::forward() 都是在 C++<utility> 中定義的( VC10 會有, VC10 CTP中沒有)。我將演示怎么來實現它們。(再次,我將交替使用 std::identity 和我的 Identity, std::forward() 和我的 Forward(),因為他們的實現是等價的。)
現在,讓我們來揭開“魔術“的神秘面紗,其實它靠的就是模板參數推導和引用折疊(reference collapsing)技術。
rvalue 引用:模板參數推導和引用折疊(reference collapsing)
rvalue 引用與模板以一種特別的方式相互作用。下面是一個示例:
C:\Temp>type collapse.cpp
#include <iostream>
#include <ostream>
#include <string>
using namespace std;
template <typename T> struct Name;
template <> struct Name<string> {
??? static const char * get() {
??????? return "string";
??? }
};
template <> struct Name<const string> {
??? static const char * get() {
??????? return "const string";
??? }
};
template <> struct Name<string&> {
??? static const char * get() {
??????? return "string&";
??? }
};
template <> struct Name<const string&> {
??? static const char * get() {
??????? return "const string&";
??? }
};
template <> struct Name<string&&> {
??? static const char * get() {
??????? return "string&&";
??? }
};
template <> struct Name<const string&&> {
??? static const char * get() {
??????? return "const string&&";
??? }
};
template <typename T> void quark(T&& t) {
??? cout << "t: " << t << endl;
??? cout << "T: " << Name<T>::get() << endl;
??? cout << "T&&: " << Name<T&&>::get() << endl;
??? cout << endl;
}
string strange() {
??? return "strange()";
}
const string charm() {
??? return "charm()";
}
int main() {
??? string up("up");
??? const string down("down");
??? quark(up);
??? quark(down);
??? quark(strange());
??? quark(charm());
}
C:\Temp>cl /EHsc /nologo /W4 collapse.cpp
collapse.cpp
C:\Temp>collapse
t: up
T: string&
T&&: string&
t: down
T: const string&
T&&: const string&
t: strange()
T: string
T&&: string&&
t: charm()
T: const string
T&&: const string&&
這里藉由 Name 的顯式規格說明來打印出類型。
當我們調用 quark(up) 時,會進行模板參數推導。 quark() 是一個帶有模板參數 T 的模板函數,但是我們還沒有為它提供顯式的類型參數(比如像 quark<X>(up)這樣的)。通過比較函數形參類型 Type&& 和函數實參類型(一個 string 類型的 lvalue)我們就能推導出模板實參類型。(譯注:原文用 argument 表示實參,parameter 表示形參)
C++0x 會轉換函數實參的類型和形參的類型,然后再進行匹配。
首先,轉換函數實參的類型。這遵循一條特殊規則(提案N2798
然后,轉換函數形參的類型。不管是 C++98/03 還是 C++0x 都會解除引用( lvalue 引用和 rvalue 引用在 C++0x 中都會被解除掉)。在前面例子的四種情形中,這樣我們會把 T&& 轉換成 T 。
于是, T 會被推導成函數實參轉換之后的類型。up 和 down 都是 lvalue,它們遵循那條特殊規則,這就是為什么 quark(up)? 打印出"T:string&" ,而 quark(down) 打印出 "T: cosnt string&"的原因。strange() 和 charm() 都是右值,它們遵循一般規則,這就是為什么 quark(strange()) 打印出 "T: string" 而 quark(charm()) 打印出"T: const string" 的原因。
替換操作會在類型推導之后進行。模板形參 T 出現的每一個地方都會被替換成推導出來的模板實參類型。在 quark(string()) 中 T 是 string ,因此 T&& 會是 string&& 。同樣,在 quark(charm()) 中,T 是 const string , 因此 T&& 是 const string&& 。但 quark(up) 和 quark(down) 不同,它們遵循另外的特殊規則。
在 quark(up) 中, T 是 string& 。進行替換的話 T&& 就成了 string& && ,在 C++0x 中會折疊(collapse)引用的引用,引用折疊的規則就是“lvalue 引用是傳染性的”。 X& &, X& && 和 X&& & 都會被折疊成 X& ,只有 X&& && 會被折疊成 X&& 。因此 string& && 被折疊成 string& 。在模板世界里,那些看起來像 rvalue 引用的東西并不一定真的就是。 因而 quark(up) 被實例化為 quark<string&>() ,進而 T&& 經替換與折疊之后變成 string& 。我們可以調用 Name<T&&>::get() 來驗證這個。 同樣, quark(down) 被實例化為 quark<const string&>() ,進而 T&& 經替換與折疊之后變成 const string& 。在 C++98/03中,你可能習慣了常量性(constness)隱藏于模板形參中(也就是說可以傳 const Foo 對象作實參來調用形參為 T& 的模板函數,就像 T& 會是 const Foo& 一樣),在 C++0x 中,左值屬性(lvalueness) 也能隱藏于模板形參中。
那好,這兩條特殊規則對我們有什么影響?在 quark() 內部,類型 T&& 有著和傳給 quark() 的實參一樣的左/右值屬性(lvalueness/rvalueness)和常量性。這樣 rvalue 引用就能保持住左右值屬性和常量性,做到完美轉發。
完美轉發: std::forward() 和 std::identidy 是怎樣工作的
讓我們再來看看 outer() :
template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) {
??? inner(Forward<T1>(t1), Forward<T2>(t2));
}
現在我們明白了為什么 outer() 的形參是 T1&& 和 T2&& 類型的了,因為它們能夠保持住傳給 outer() 的實參的信息。那為什么這里要調用 Forward<T1>() 和 Forward<T2>() 呢?還記得么,具名 lvalue 引用和具名 rvalue 引用都是 lvalue 。如果 outer() 調用 inner(t1, t2) ,那么 inner() 總是會當 lvalue 來引用 t1 和 t2 ,這就破壞了完美轉發。
幸 運的是,不具名 lvalue 引用是 lvalue,不具名 rvalue 引用還是 rvalue 。因此,為了將 t1 和 t2 轉發給 inner(),我們需要將它們傳到一個幫助函數中去,這個幫助函數移除它們的名字,保持住它們的屬性信息。這就是 std::forward() 做的事情:
template <typename T> struct Identity {
??? typedef T type;
};
template <typename T> T&& Forward(typename Identity<T>::type&& t) {
??? return t;
}
當我們調用 Forward<T1>(t1) , Identidy 并沒有修改 T1 (很快我們講到 Identidy 對 T1 做了什么)。因此 Forward<T1>() 接收 T1&& ,返回 T1&& 。這樣就移除了 t1 的名字,保持住 t1 的類型信息(而不論 t1 是什么類型, string& 也好, const string& 也好, string&& 也好或 const string&& 也好)。這樣 inner() 看到的 Forward<T1>(t1) ,與 outer() 接收的第一個實參有著相同的信息,包括類型,lvalueness/rvalueness,常量性等等。完美轉發就是這樣工作的。
你可能會好奇如果不小心寫成 Forward<T1&&>(t1) 又會怎樣呢?(這個錯誤還是蠻誘人的,因為 outer() 接收的就是 T1&& t1 )。很幸運,沒什么壞事情會發生。 Forward<T1&&>() 接收與返回的都是 T1&& && ,這會被折疊成 T1&& 。于是,Forward<T1>(t1) 和 Forward<T1&&>(t1) 是等價的,我們更偏好前者,是因為它要短些。
Identidy 是做什么用的呢?為什么下面的代碼不能工作?
template <typename T> T&& Forward(T&& t) { // BROKEN
??? return t;
}
如果 Forward() 像是上面那樣,它就能被隱式調用(不帶明確的模板參數)。當我們傳給 Forward() 一個 lvalue 實參時,模板參數推導就介入了,如我們前面看到的那樣會將 T&& 變成 T&,也就是變成一個 lvalue 引用。問題來了,即使形參 T1&& 和 T2&& 指明是 rvalue 引用,但在 outer() 中,具名的 t1 和 t2 卻是 lvaue ,這個問題是我們一直想要解決的!使用上面那個錯誤的實現, Forward<T1>(t1) 是可以工作的,而 Foarward(t1) 雖然能通過編譯(很誘人哦)但會出錯,就如它就是 t1 一樣。真是痛苦的源泉啊,因此,Identity 被用來阻止模板參數推導。typename Identity<T>::type 中的那對冒號就像絕緣體,模板參數推導無法穿越它,有模板編程經驗的程序員應該對此很熟悉了,因為這在 C++98/03 和 C++0x 中是一樣的。(要解釋這個是另外的事情了)
//.//
下篇:
文 / 李博(光宇廣貞)
?????? 《C++ 0x 之左值與右值》文中提到 std::forward() 和 std::move()。本文開頭對之補充一句:
??? 在操作函數返回值或函數參數時,匿名左值仍然為左值,左值可以具名;匿名右值仍然為右值,右值一旦具名成功,立即轉變為左值。
?????? 舉一個例子。使用 std::move() 方法向 Outer 傳遞右值后,使用 std::forward() 保證調用右值重載,而直接訪問參數 t 的調用綁定到了左值重載。見下圖:
?????? 因此,操作右值引用不能直接操作其變量名,否則將使右值引用具名,從而轉為左值引用。操作右值引用必須使用 std::move()、std::forword() 等方法。將兩個方法的實體展開如下:
?????? 這兩個 identity 和 Remove_Reference 是干嘛用的?若不要它們,直接用 T&& 做為 forward 里 arg 的類型或 move 里返回類型呢?
?????? 不行。首先說 forward 方法。重新看如下代碼:
?????? template < typename T > void Outer ( T&& t )
?????? {
????????????? Inner ( std::forward<T> ( t ) );
?????? }
?????? 在《C++ 0x 之左值與右值》文中提到,我們使用 forward 的目的是保證參數的左右值性和只讀性的準確傳導。若不使用 identity 而只使用 T&& 的話,調用 std::forward<T>(t) 仍然沒有問題;而當調用 std::forward(t),即不指明模板參數類型時,T&& 將由 t 推導,問題便來了。注意到 t 在傳參數時,是點名調用,使 t 具名,故而無論原來 t 是左值還是右值,此刻都將視為左值,從而 T 被推導為左值引用,且 T&& 歸化為左值引用,于是 forward 方法以左值引用類型接收參數 t,并以左值作為其返回類型。如是便違背了 forward 的本意。
?????? 而使用 identity 后,type 被指定為 T。注意這里,“::”算符就像一面墻,擋住了類型推導看見其左側。因此,編譯器不會認為 arg 參數是需要類型推導的,是被 identity::type 指定類型的(當然也強制使用 forward 時要注明模板參數,否則將無從推導)。這就保證了 forward 總是以右值引用類型接收參數 t。前文已經提到右值引用類型參數可以保留實參的一切信息。如是保證了 forward 的本意的實現。
?????? 最后,move 方法里面的 Remove_Reference 就好解釋了。模板將根據 arg 推導類型,若 arg 是 const Type& 型,則 T 推導為 const Type& 型,而后代入特化模板,使 type 為 const Type,從而返回類型為 const Type&& 型。若 arg 是右值類型,則 type 也將歸化為右值引用型。總之,Remove_Reference 模板類保證返回類型為右值引用。
?????? 所屬分類:C++
?????? 參考:
?????? C++ 0x 之 Lambda:賢妻與嬌娃,你娶誰當老婆?聽 FP 如何點化 C++
?????? 從 C++ 模板元編程生產質數看 F# 函數式編程思想
?????? 人類最偉大的王安石時代,自古至今乃至不可預見的未來
?????? 天生是兵家——一代神將“蔣方震”
?????? 漢族姓氏與基因——為何有“兩個漢族”?同姓是否同源?隨父隨母、生男生女是否一樣?
?????? 緬甸攻打果敢漢人與中印邊界對峙的關系——東段麥克馬洪線!
?????? 限次連續若干同色球概率算法
?????? Vista 高級掃雷 105 秒到 135 秒各記錄截圖
?????? 做為中國人,以下“歷史片斷”絕對顛覆你的想像!
?????? 王夫人向賈母挑戰
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀
總結
以上是生活随笔為你收集整理的std:forward 完美转发的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: std::move C++11 标准
- 下一篇: 平面旋转的折叠定理