第15課 完美轉發(std::forward)


一、理解引用折疊 

(一)引用折疊

  1. 在C++中,“引用的引用”是非法的。像auto& &rx = x;(注意兩個&之間有空格)這種直接定義引用的引用是不合法的,但是編譯器在通過類型別名或模板參數推導等語境中,會間接定義出“引用的引用”,這時引用會形成“折疊”。

  2. 引用折疊會發生在模板實例化、auto類型推導、創建和運用typedef和別名聲明、以及decltype語境中

(二)引用折疊規則

  1. 兩條規則

    (1)所有右值引用折疊到右值引用上仍然是一個右值引用。如X&& &&折疊為X&&。

    (2)所有的其他引用類型之間的折疊都將變成左值引用。如X& &, X& &&, X&& &折疊為X&。可見左值引用會傳染,沾上一個左值引用就變左值引用了根本原因:在一處聲明為左值,就說明該對象為持久對象,編譯器就必須保證此對象可靠(左值)

  2. 利用引用折疊進行萬能引用初始化類型推導

    (1)當萬能引用(T&& param)綁定到左值時,由於萬能引用也是一個引用,而左值只能綁定到左值引用。因此,T會被推導為T&類型。從而param的類型為T& &&,引用折疊后的類型為T&。

    (2)當萬能引用(T&& param)綁定到右值時,同理,右值只能綁定到右值引用上,故T會被推導為T類型。從而param的類型就是T&&(右值引用)。

【編程實驗】引用折疊

#include <iostream>

using namespace std;

class Widget{};

template<typename T>
void func(T&& param){}

//Widget工廠函數
Widget widgetFactory() 
{
    return Widget();
}

//類型別名
template<typename T>
class Foo
{
public:
    typedef T&& RvalueRefToT;
};

int main()
{
    int x = 0;
    int& rx = x;
    //auto& & r = x; //error,聲明“引用的引用”是非法的!

    //1. 引用折疊發生的語境1——模板實例化
    Widget w1;
    func(w1); //w1為左值,T被推導為Widget&。代入得void func(Widget& && param);
              //引用折疊后得void func(Widget& param)

    func(widgetFactory()); //傳入右值,T被推導為Widget,代入得void func(Widget&& param)
                           //注意這里沒有發生引用的折疊。

    //2. 引用折疊發生的語境2——auto類型推導
    auto&& w2 = w1; //w1為左值auto被推導為Widget&,代入得Widget& && w2,折疊后為Widget& w2
    auto&& w3 = widgetFactory(); //函數返回Widget,為右值,auto被推導為Widget,代入得Widget w3

    //3. 引用折疊發生的語境3——tyedef和using
    Foo<int&> f1;  //T被推導為int&,代入得typedef int& && RvalueRefToT;折疊后為typedef int& RvalueRefToT

    //4. 引用折疊發生的語境3——decltype
    decltype(x)&& var1 = 10;  //由於x為int類型,代入得int&& rx。
    decltype(rx) && var2 = x; //由於rx為int&類型,代入得int& && var2,折疊后得int& var2

    return 0;
}
引用折疊示例代碼

二、完美轉發

(一)std::forward原型

template<typename T>
T&& forward(typename remove_reference<T>::type& param)
{
    return static_cast<T&&>(param); //可能會發生引用折疊!
}

(二)分析std::forward<T>實現條件轉發的原理(以轉發Widget類對象為例

 

  1. 當傳遞給func函數的實參類型左值Widget時,T被推導為Widget&類別。然后forward會實例化為std::forward<Widget&>,並返回Widget&(左值引用,根據定義是個左值!

  2. 而當傳遞給func函數的實參類型右值Widget時,T被推導為Widget。然后forward被實例化為std::forward<Widget>,並返回Widget&&(注意,匿名的右值引用是個右值!)

  3. 可見,std::forward會根據傳遞給func函數實參(注意,不是形參)的左/右值類型進行轉發當傳給func函數左值實參時,forward返回左值引用,並將該左值轉發給process。而當傳入func的實參為右值時,forward返回右值引用,並將該右值轉發給process函數。

【編程實驗】不完美轉發和完美轉發

#include <iostream>
using namespace std;

void print(const int& t)  //左值版本
{
    cout <<"void print(const int& t)" << endl;
}

void print(int&& t)     //右值版本
{
    cout << "void print(int&& t)" << endl;
}

template<typename T>
void testForward(T&& param)
{
    //不完美轉發
    print(param);            //param為形參,是左值。調用void print(const int& t)
    print(std::move(param)); //轉為右值。調用void print(int&& t)

    //完美轉發
    print(std::forward<T>(param)); //只有這里才會根據傳入param的實參類型的左右值進轉發
}

int main()
{
    cout <<"-------------testForward(1)-------------" <<endl;
    testForward(1);    //傳入右值

    cout <<"-------------testForward(x)-------------" << endl;
    int x = 0;
    testForward(x);    //傳入左值

    return 0;
}
/*輸出結果
-------------testForward(1)-------------
void print(const int& t)
void print(int&& t)
void print(int&& t)       //完美轉發,這里轉入的1為右值,調用右值版本的print
-------------testForward(x)-------------
void print(const int& t)
void print(int&& t)
void print(const int& t) //完美轉發,這里轉入的x為左值,調用左值版本的print
*/
不完美轉發和完美轉發示例代碼

三、std::move和std::forward

(一)兩者比較

  1. move和forward都是僅僅執行強制類型轉換的函數。std::move無條件地將實參強制轉換成右值。而std::forward則僅在某個特定條件滿足時(傳入func的實參是右值時)才執行強制轉換

  2. std::move並不進行任何移動,std::forward也不進行任何轉發。這兩者在運行期都無所作為。它們不會生成任何可執行代碼,連一個字節都不會生成。

(二)使用時機

  1. 針對右值引用的最后一次使用實施std::move,針對萬能引用的最后一次使用實施std::forward

  2. 在按值返回的函數中,如果返回的是一個綁定到右值引用或萬能引用的對象時,可以實施std::move或std::forward。因為如果原始對象是一個右值,它的值就應當被移動到返回值上,而如果是左值,就必須通過復制構造出副本作為返回值。

(三)返回值優化(RVO)

  1.兩個前提條件

    (1)局部對象類型函數返回值類型相同

    (2)返回的就是局部對象本身(含局部對象或作為return 語句中的臨時對象等)

  2. 注意事項

    (1)在RVO的前提條件被滿足時,要么避免復制要么會自動地用std::move隱式實施於返回值

    (2)按值傳遞的函數形參,把它們作為函數返回值時,情況與返回值優化類似。編譯器這里會選擇第2種處理方案,即返回時將形參轉為右值處理

    (3)如果局部變量有資格進行RVO優化,就不要把std::move或std::forward用在這些局部變量中。因為這可能會讓返回值喪失優化的機會。

【編程實驗】RVO優化和std::move、std::forward

#include <iostream>
#include <memory>
using namespace std;

//1. 針對右值引用實施std::move,針對萬能引用實施std::forward
class Data{};

class Widget
{
    std::string name;
    std::shared_ptr<Data> ptr;
public:
    Widget() { cout <<"Widget()"<<endl; };

    //復制構造函數
    Widget(const Widget& w):name(w.name), ptr(w.ptr)
    {
        cout <<"Widget(const Widget& w)" << endl;
    }
    //針對右值引用使用std::move
    Widget(Widget&& rhs) noexcept: name(std::move(rhs.name)), ptr(std::move(rhs.ptr))
    {
        cout << "Widget(Widget&& rhs)" << endl;
    }

    //針對萬能引用使用std::forward。
    //注意,這里使用萬能引用來替代兩個重載版本:void setName(const string&)和void setName(string&&)
    //好處就是當使用字符串字面量時,萬能引用版本的效率更高。如w.setName("SantaClaus"),此時字符串會被
    //推導為const char(&)[11]類型,然后直接轉給setName函數(可以避免先通過字量面構造臨時string對象)。
    //並將該類型直接轉給name的構造函數,節省了一個構造和釋放臨時對象的開銷,效率更高。
    template<typename T>
    void setName(T&& newName)
    {
        if (newName != name) { //第1次使用newName
            name = std::forward<T>(newName); //針對萬能引用的最后一次使用實施forward
        }
    }
};

//2. 按值返回函數
//2.1 按值返回的是一個綁定到右值引用的對象
class Complex 
{
    double x;
    double y;
public:
    Complex(double x =0, double y=0):x(x),y(y){}
    Complex& operator+=(const Complex& rhs) 
    {
        x += rhs.x;
        y += rhs.y;
        return *this;
    }
};

Complex operator+(Complex&& lhs, const Complex& rhs) //重載全局operator+
{
    lhs += rhs;
    return std::move(lhs); //由於lhs綁定到一個右值引用,這里可以移動到返回值上。
}

//2.2 按值返回一個綁定到萬能引用的對象
template<typename T>
auto test(T&& t)
{
    return std::forward<T>(t); //由於t是一個萬能引用對象。按值返回時實施std::forward
                               //如果原對象一是個右值,則被移動到返回值上。如果原對象
                               //是個左值,則會被拷貝到返回值上。
}

//3. RVO優化
//3.1 返回局部對象
Widget makeWidget()
{
    Widget w;

    return w;  //返回局部對象,滿足RVO優化兩個條件。為避免復制,會直接在返回值內存上創建w對象。
               //但如果改成return std::move(w)時,由於返回值類型不同(Widget右值引用,另一個是Widget)
               //會剝奪RVO優化的機會,就會先創建w局部對象,再移動給返回值,無形中增加一個移動操作。
               //對於這種滿足RVO條件的,當某些情況下無法避免復制的(如多路返回),編譯器仍會默認地對
               //將w轉為右值,即return std::move(w),而無須用戶顯式std::move!!!
}

//3.2 按值形參作為返回值
Widget makeWidget(Widget w) //注意,形參w是按值傳參的。
{
    //...

    return w; //這里雖然不滿足RVO條件(w是形參,不是函數內的局部對象),但仍然會被編譯器優化。
              //這里會默認地轉換為右值,即return std::move(w)
}

int main()
{
    cout <<"1. 針對右值引用實施std::move,針對萬能引用實施std::forward" << endl;
    Widget w;
    w.setName("SantaClaus");

    cout << "2. 按值返回時" << endl;
    auto t1 = test(w); 
    auto t2 = test(std::move(w));

    cout << "3. RVO優化" << endl;
    Widget w1 = makeWidget();   //按值返回局部對象(RVO)
    Widget w2 = makeWidget(w1); //按值返回按值形參對象

    return 0;
}
/*輸出結果
1. 針對右值引用實施std::move,針對萬能引用實施std::forward
Widget()
2. 按值返回時
Widget(const Widget& w)
Widget(Widget&& rhs)
3. RVO優化
Widget()
Widget(Widget&& rhs)
Widget(const Widget& w)
Widget(Widget&& rhs)
*/

四、完美轉發失敗的情形

(一)完美轉發失敗

  1. 完美轉發不僅轉發對象,還轉發其類型、左右值特征以及是否帶有const或volation等修飾詞。而完美轉發的失敗,主要源於模板類型推導失敗或推導的結果是錯誤的類型。

  2. 實例說明:假設轉發的目標函數f,而轉發函數為fwd(天然就應該是泛型)。函數如下:

template<typename… Ts>
void fwd(Ts&&… params)
{
     f(std::forward<Ts>(params)…);
}

f(expression);    //如果本語句執行了某操作
fwd(expression);  //而用同一實參調用fwd則會執行不同操作,則稱完美轉發失敗。

(二)五種完美轉發失敗的情形

  1. 使用大括號初始化列表時

  (1)失敗原因分析:由於轉發函數是個模板函數,而在模板類型推導中,大括號初始不能自動被推導為std::initializer_list<T>

  (2)解決方案:先用auto聲明一個局部變量,再將該局部變量傳遞給轉發函數。

  2. 0和NULL用作空指針時

  (1)失敗原因分析:0或NULL以空指針之名傳遞給模板時,類型推導的結果是整型而不是所希望的指針類型。

  (2)解決方案:傳遞nullptr,而非0或NULL。

  3. 僅聲明static const 整型成員變量,而無其定義時。

  (1)失敗原因分析:C++中常量一般是進入符號表的,只有對其取地址時才會實際分配內存。調用f函數時,其實參是直接從符號表中取值,此時不會發生問題。但當調用fwd時由於其形參是萬能引用,而引用本質上是一個可解引用的指針。因此當傳入fwd時會要求准備某塊內存以供解引用出該變量出來但因其未定義,也就沒有實際的內存空間, 編譯時可能失敗(取決於編譯器和鏈接器的實現)。

  (2)解決方案:在類外定義該成員變量。注意這聲變量在聲明時一般會先給初始值。因此定義時無需也不能再重復指定初始值

  4. 使用重載函數名或模板函數名

  (1)失敗原因分析:由於fwd是個模板函數,其形參沒有任何關於類型的信息。當傳入重載函數名或模板函數(代表許許多多的函數)時,就會導致fwd的形參不知綁定到哪個函數上。

  (2)解決方案:在調用fwd調用時手動為形參指定類型信息。

  5. 轉發位域時

  (1)失敗原因分析:位域是由機器字的若干任意部分組成的(如32位int的第3至5個比特),但這樣的實體是無法直接取地址的。而fwd的形參是個引用,本質上就是指針,所以也沒有辦法創建指向任意比特的指針

  (2)解決方案:制作位域值的副本,並以該副本來調用轉發函數。

【編程實驗】完美轉發失敗的情形及解決方案

#include <iostream>
#include <vector>

using namespace std;

//1. 大括號初始化列表
void f(const std::vector<int>& v)
{
    cout << "void f(const std::vector<int> & v)" << endl;
}

//2. 0或NULL用作空指針時
void f(int x)
{
    cout << "void f(int x)" << endl;
}


//3. 僅聲明static const的整型成員變量而無定義
class Widget
{
public:
    static const  std::size_t MinVals = 28; //僅聲明,無定義(因為靜態變量需在類外定義!)
};

//const std::size_t Widget::MinVals; //在類外定義,無須也不能重復指定初始值。

//4. 使用重載函數名或模板函數名
int f(int(*pf)(int))
{
    cout <<"int f(int(*pf)(int))" << endl;
    return 0;
}

int processVal(int value) { return 0; }
int processVal(int value, int priority) { return 0; }

//5.位域
struct IPv4Header
{
    std::uint32_t version : 4,
                  IHL : 4,
                  DSCP : 6,
                  ECN : 2,
                  totalLength : 16;
    //...
};

template<typename T>
T workOnVal(T param)  //函數模板,代表許許多多的函數。
{
    return param;
}

//用於測試的轉發函數
template<typename ...Ts>
void fwd(Ts&& ... param)  //轉發函數
{
    f(std::forward<Ts>(param)...);  //目標函數
}

int main()
{
    cout <<"-------------------1. 大括號初始化列表---------------------" << endl;    
    //1.1 用同一實參分別調用f和fwd函數
    f({ 1, 2, 3 });  //{1, 2, 3}會被隱式轉換為std::vector<int>
    //fwd({ 1, 2, 3 }); //編譯失敗。由於fwd是個函數模板,而模板推導時{}不能自動被推導為std:;initializer_list<T>
    //1.2 解決方案
    auto il = { 1,2,3 };
    fwd(il);

    cout << "-------------------2. 0或NULL用作空指針-------------------" << endl;
    //2.1 用同一實參分別調用f和fwd函數
    f(NULL);   //調用void f(int)函數,
    fwd(NULL); //NULL被推導為int,仍調用void f(int)函數
    //2.2 解決方案:使用nullptr
    f(nullptr);  //匹配int f(int(*pf)(int))
    fwd(nullptr);

    cout << "-------3. 僅聲明static const的整型成員變量而無定義--------" << endl;
    //3.1 用同一實參分別調用f和fwd函數
    f(Widget::MinVals);   //調用void f(int)函數。實參從符號表中取得,編譯成功!
    fwd(Widget::MinVals); //fwd的形參是引用,而引用的本質是指針,但fwd使用到該實參時需要解引用
                          //這里會因沒有為MinVals分配內存而出現編譯失敗(取決於編譯器和鏈接器)
    //3.2 解決方案:在類外定義該變量

    cout << "-------------4. 使用重載函數名或模板函數名---------------" << endl;
    //4.1 用同一實參分別調用f和fwd函數
    f(processVal);   //ok,由於f形參為int(*pf)(int),帶有類型信息,會匹配int processVal(int value)
    //fwd(processVal); //error,fwd的形參不帶任何類型信息,不知該匹配哪個processVals重載函數。
    //fwd(workOnVal);  //error,workOnVal是個函數模板,代表許許多多的函數。這里不知綁定到哪個函數
    //4.2 解決方案:手動指定類型信息
    using ProcessFuncType = int(*)(int);
    ProcessFuncType processValPtr = processVal;
    fwd(processValPtr);
    fwd(static_cast<ProcessFuncType>(workOnVal));   //調用int f(int(*pf)(int))

    cout << "----------------------5. 轉發位域時---------------------" << endl;
    //5.1 用同一實參分別調用f和fwd函數
    IPv4Header ip = {};
    f(ip.totalLength);  //調用void f(int)
    //fwd(ip.totalLength); //error,fwd形參是引用,由於位域是比特位組成。無法創建比特位的引用!
    //解決方案:創建位域的副本,並傳給fwd
    auto length = static_cast<std::uint16_t>(ip.totalLength);
    fwd(length);
    
    return 0;
}
/*輸出結果
-------------------1. 大括號初始化列表---------------------
void f(const std::vector<int> & v)
void f(const std::vector<int> & v)
-------------------2. 0或NULL用作空指針-------------------
void f(int x)
void f(int x)
int f(int(*pf)(int))
int f(int(*pf)(int))
-------3. 僅聲明static const的整型成員變量而無定義--------
void f(int x)
void f(int x)
-------------4. 使用重載函數名或模板函數名---------------
int f(int(*pf)(int))
int f(int(*pf)(int))
int f(int(*pf)(int))
----------------------5. 轉發位域時---------------------
void f(int x)
void f(int x)
*/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM