程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> Effective Modern C++ 條款28 理解引用折疊

Effective Modern C++ 條款28 理解引用折疊

編輯:關於C++

理解引用折疊

條款23提起過把一個實參傳遞給模板函數時,無論實參是左值還是右值,推斷出來的模板參數都會含有編碼信息。那條款沒有提起,只有模板形參是通用引用時,這件事才會發生,不過對於這疏忽,理由很充分:條款24才介紹通用引用。把這些關於通用引用和左值/右值編碼信息綜合,意味著這個模板,

template
void func(T&& param);

無論param是個左值還是右值,需要推斷的模板參數T都會被編碼。

編碼技術是很簡單的,當傳遞的實參是個左值時,T就被推斷為一個左值引用,當傳遞的實參是個右值時,T就被推斷為一個非引用類型。(注意這是不對稱的:左值會編碼為左值引用,但右值編碼為非引用。)因此:

Widget widgetFactory();      // 返回右值的函數

Widget w;       // 一個變量,左值

func(w);      // 用左值調用函數,T被推斷為Widget&

func(widgetFactory());     // 用右值調用函數,T被推斷為Widget

兩個func調用都是用Widget參數,不過一個Widget是左值,另一個是右值,從而導致了模板參數T被推斷出不同的類型。這,正如我們將很快看到,是什麼決定通用引用變成左值引用或右值引用的,而這也是std::forward完成工作所使用的內部技術。

在我們緊密關注std::forward和通用引用之前,我們必須注意到,在C++中對引用進行引用是不合法的。你可以嘗試聲明一個,你的編譯器會嚴厲譴責加抗議:

int x;
...
auto& & rx = x;    // 報錯!不可以聲明對引用的引用

但想一想當一個左值被傳遞給接受通用引用的模板函數時:

template
void func(T&& param);      // 如前

func(w);     // 用左值調用func,T被推斷為Widget&

如果使用推斷出來的T類型(即Widget&)實例化模板,我們得到這個:

void func(Widget& && param);

一個對引用的引用!然而你的編譯器內有深刻譴責加抗議。我們從條款24知道,通用引用param用一個左值進行初始化,param的類型應該出一個左值引用,但編譯器是如何推斷T的類型的,還有是怎樣把它替代成下面這個樣子,哪一個才是最終的簽名呢?

void func(Widget& param);

答案是引用折疊。是的,你是禁止聲明對引用的引用,但編譯器在特殊的上下文中可以產生它們,模板實例化就是其中之一。當編譯器生成對引用的引用時,引用折疊指令就會隨後執行。

有兩種類型的引用(左值和右值),所以有4種可能的對引用引用的組合(左值對左值,左值對右值,右值對左值,右值對右值)。如果對引用的引用出現在被允許的上下文(例如,在模板實例化時),這個引用(即引用的引用,兩個引用)會折疊成一個引用,根據的是下面的規則:

如果兩個引用中有一個是左值引用,那麼折疊的結果是一個左值引用。否則(即兩個都是右值引用),折疊的結果是一個右值引用。

在我們上面的例子中,在函數func中把推斷出來的類型Widget&替代T後,產生了一個對右值的左值引用,然後引用折疊規則告訴我們結果是個左值引用。

引用折疊是使std::forward工作的關鍵部分。就如條款25解釋那樣,對通用引用使用std::forward,是一種常見的情況,像這樣:

template
void f(T&& fParam)
{
    ...        // do some works

    someFunc(std::forward(fParam));   // 把fParam轉發到someFunc
}

因為fParam是一個通用引用,我們知道無論傳遞給函數f的實參(即用來初始化fParam的表達式)是左值還是右值,參數類型T都會被編碼。std::forward的工作是,當且僅當傳遞給函數f的實參是個右值時,把fParam(左值)轉換成一個右值。

這裡是如何實現std::forward來完成工作:

template          // 在命名空間std中
T&& forward(typename remove_reference::type& param)
{
    return static_cast(param);
}

這沒有完全順應標准庫(我省略了一些接口細節),不過不同的部分是與理解std::forward如何工作無關。

假如傳遞給函數f的是個左值的Widget,T會被推斷為Widget&,然後調用std::forward會讓它實例化為std::forward。把Widget&加到std::forward的實現中,變成這樣:

Widget& && forward(typename remove_reference::type& param)
{
    return static_cast(param);
}

remove_reference::type產生的是Widget,所以std::forward邊沖這樣:

Widget& && forward(Widget& param)
{    return static_cast(param);    }

在返回類型和cast中都會發生引用折疊,導致被調用的最終版本的std::forward

Widget& forward(Widget& param)
{    return static_cast(param);    }

就如你所見,當一個左值被傳遞給模板函數f時,std::forward被實例化為接受一個左值引用和返回一個左值引用。std::forward內部的顯式轉換沒有做任何東西,因為param的類型已經是Widget&了,所以這次轉換沒造成任何影響。一個左值實參被傳遞給std::forward,將會返回一個左值引用。根據定義,左值引用是左值,所以傳遞一個左值給std::forward,會導致std::forward返回一個左值,就跟它應該做的那樣。

現在假設傳遞給函數f的是個右值的Widget。在這種情況下,函數f的類型參數T會被推斷為Widget。因此f裡面的std::forward會變成std::forward。在std::forward的實現中用Widget代替T,像這樣:

Widget&& forward(typename remove_reference::type& param)
{
    return static_cast(param);
}

對非引用Widget使用std::remove_reference會產生原來的類型(Widget),所以std::forward變成這樣:

Widget&& forward(Widget& param)
{    return static_cast(param);    }

這裡沒有對引用的引用,所以沒有進行引用折疊,這也就這次std::forward調用的最終實例化版本。

由函數返回的右值引用被定義為右值,所以在這種情況下,std::forward會把f的參數fParam(一個左值)轉換成一個右值。最終結果是傳遞給函數f的右值實參作為右值被轉發到someFunc函數,這是順理成章的事情。

在C++14中,std::remove_reference_t的存在可以讓std::forward的實現變得更簡潔:

template      // C++14,在命名空間std中
T&& forward(remove_reference_t& param)
{
    return static_cast(param);
}

引用折疊出現在四種上下文。第一種是最常見的,就是模板實例化。第二種是auto變量的類型生成。它的細節本質上和模板實例化相同,因為auto變量的類型推斷和模板類型推斷本質上相同(看條款2)。再次看回之前的一個例子:


template
void func(T&& param);

Widget widgetFactory();      // 返回右值的函數

Widget w;       // 一個變量,左值

func(w);      // 用左值調用函數,T被推斷為Widget&

func(widgetFactory());     // 用右值調用函數,T被推斷為Widget

這可以用auto形式模仿。這聲明

auto&& w1 = w;

用個左值初始化w1,因此auto被推斷為Widget&。在聲明中用Widget&代替auto聲明w1,產生這個對引用進行引用的代碼,

Widget& && w1 = w;

這在引用折疊之後,變成

Widget& w1 = w;

結果是,w1是個左值引用。

另一方面,這個聲明

auto&&w2 = widgetFactory();

用個右值初始化w2,導致auto被推斷為無引用類型Widget,然後用Widget替代auto變成這樣:

Widget&& w2 = widgetFactory();

這裡沒有對引用的引用,所以我們已經完成了,w2是個右值引用。

我們現在處於真正能理解條款24介紹通用引用的位置了。通用引用不是一種新的引用類型,實際上它是右值引用——在滿足了下面兩個條件的上下文中:

根據左值和右值來進行類型推斷。T類型的左值使T被推斷為T&,T類型的右值使T被推斷為T。 發生引用折疊

通用引用的概念是很有用的,因為它讓你免受:識別出存在引用折疊的上下文,弱智地根據左值和右值推斷上下文,然後弱智地把推斷出的類型代進上下文,最後使用引用折疊規則。

我說過有4中這樣的上下文,不過我們只討論了兩種:模板實例化和auto類型生成。第三種上下文就是使用typedef和類型別名聲明(看條款9)。如果,在typedef創建或者評估期間,出現了對引用的引用,引用折疊會出面消除它們。例如,假如我們有個類模板Widget,內部嵌有一個右值引用類型的typedef

template
class Widget {
public:
 typedef T&& RvalueRefToT;
 ...
};

然後假如我們用一個左值引用來實例化Widget:

Widget w;

在Widget中用int&代替T,typedef變成這樣:

typedef int& && RvalueRefToT;

引用折疊把代碼弄出這樣:

type int& RvalueRefToT;

這很明顯的告訴我們,我們typedef選擇的名字跟我們期望得到的不一樣:當用左值引用實例化Widget時,RvalueRefToT是個左值引用的typedef

最後的一種會發生引用折疊的上下文是使用decltype中。如果,在分析一個使用decltype的類型期間,出現了對引用的引用,引用折疊會出面消除它。(關於decltype的詳細信息,請看條款3。)

總結

需要記住的3點:

引用折疊會出現在4中上下文:模板實例化,auto類型生成,typedef和類型別名聲明的創建和使用,decltype。 當編譯器在一個引用折疊上下文中生成了對引用的引用時,結果會變成一個引用。如果原來的引用中有一個是左值引用,結果就是個左值引用。否則,結果是個右值引用。 通用引用是——出現在類型推斷區分左值和右值和出現引用折疊的上下文中的——右值引用。
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved