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

理解引用摺疊

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

template<typename T>
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<typename T>
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<typename T>
void f(T&& fParam)
{
    ...        // do some works

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

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

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

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

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

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

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

remove_reference<Widget&>::type產生的是Widget,所以std::forward邊衝這樣:

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

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

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

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

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

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

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

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

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

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

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

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

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


template<typename T>
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<typename T>
class Widget {
public:
    typedef T&& RvalueRefToT;
    ...
};

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

Widget<int&> w;

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

typedef int& && RvalueRefToT;

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

type int& RvalueRefToT;

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

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

總結

需要記住的3點:

  • 引用摺疊會出現在4中上下文:模板實例化,auto類型生成,typedef和類型別名聲明的創建和使用,decltype
  • 當編譯器在一個引用摺疊上下文中生成了對引用的引用時,結果會變成一個引用。如果原來的引用中有一個是左值引用,結果就是個左值引用。否則,結果是個右值引用。
  • 通用引用是——出現在類型推斷區分左值和右值和出現引用摺疊的上下文中的——右值引用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章