第15課 完美轉發

1、理解引用摺疊 ios

(一)引用摺疊express

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

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

(二)引用摺疊規則函數

  1. 兩條規則測試

    (1)全部右值引用摺疊到右值引用上仍然是一個右值引用。如X&& &&摺疊爲X&&。優化

    (2)全部的其餘引用類型之間的摺疊都將變成左值引用。如X& &, X& &&, X&& &摺疊爲X&。可見左值引用會傳染,沾上一個左值引用就變左值引用了根本緣由:在一處聲明爲左值,就說明該對象爲持久對象,編譯器就必須保證此對象可靠(左值)ui

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

    (1)當萬能引用(T&& param)綁定到左值時,因爲萬能引用也是一個引用,而左值只能綁定到左值引用。所以,T會被推導爲T&類型。從而param的類型爲T& &&,引用摺疊後的類型爲T&。spa

    (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;
}
引用摺疊示例代碼

2、完美轉發

(一)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
*/
不完美轉發和完美轉發示例代碼

3、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)
*/

4、完美轉發失敗的情形

(一)完美轉發失敗

  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)
*/
相關文章
相關標籤/搜索