第19課 lambda vs std::bind

一. std::bindios

(一)std::bind實現的關鍵技術編程

【編程實驗】探索bind原理,實現本身的bind函數less

#include <iostream>
#include <tuple>

using namespace std;

//1. 佔位符定義
template<size_t idx>
struct placeholder{}; 

template<size_t idx>
using ph = placeholder<idx>; 

constexpr ph<0> _0{}; //定義一個constexpr類型的佔位符對象(_0),並用大括號初始化。
constexpr ph<1> _1{}; //定義一個constexpr類型的佔位符對象(_1)
constexpr ph<2> _2;   //定義一個constexpr類型的佔位符對象(_2)
constexpr ph<3> _3;   //定義佔位符對象(_3)

//2. 參數選擇:do_select_param會根據是否爲佔位符來選擇合適的實參。
//2.2 泛化版本:arg不是佔位符
template<class Args, class Params>
struct do_select_param
{
    decltype(auto) operator()(Args& arg, Params&&) //arg不是佔位符,說明arg自己就是一個真正的實參,直接返回。
    {
        return arg;  //因爲arg是個引用,decltype(auto)結果也是arg的引用
    }
};

//2.3 特化:arg爲佔位符狀況。
template<size_t idx, class Params>
struct do_select_param<placeholder<idx>, Params> //注意Params爲bind返回的綁定對象被調用時,傳入其中的參數包
{
    decltype(auto) operator()(placeholder<idx>, Params&& params) //這裏的params是個tuple對象。
    {
        return std::get<idx>(std::forward<Params>(params));//根據佔位符取出params參數包中的實參。
    }
};

//2.1 根據佔位符選擇合適的實參
template<class Args, class Params>
decltype(auto) select_param(Args& arg, Params&& params)
{
    //注意,其中的arg是bind綁定時傳入的實參,多是實參或佔位符。而params是bind返回的可調用對象被執行時傳入的實參。
    //若是綁定時是佔位符,會do_select_param會分派到其特化的版本,不然分派到其泛化版本。
    return do_select_param<Args, Params>{}(arg, std::move(params)); //params是副本,統一用move而不用forwared!
}

//3. binder類及輔助函數
//3.3 綁定對象(obj)的調用: 其中args表示傳入bind函數的參數,params表示傳入obj可調用對象的參數。
template<size_t... idx, class Callable, class Args, class Params>
decltype(auto) bind_invoke(std::index_sequence<idx...>, Callable& obj, Args& args, Params&& params)
{

    //根據args是不是佔位符來選擇合適的實參,並傳給可調用對象obj。
    //注意:爲了提升效率,參數是以move的形式(右值)被傳遞給obj可調動對象的。
    return std::invoke(obj, select_param(std::get<idx>(args), std::move(params))...);//C++17, invoke(func, 參數1, 參數2, ...)
}

//3.2 binder類(核心類)
template<class Callable, class... Args>
class binder
{
    using Seq = std::index_sequence_for<Args...>;   //等價於std::make_index_sequence<sizeof...(Args)>
                                                    //會建立相似一個index_sequence<0,1,2...>的類
    //保存bind函數的全部實參(便可調用對象及其實參)
    using args_t = std::tuple<std::decay_t<Args>...>; //注意,decay_t去掉其引用、const\volatile等特性)
    using callable_t = std::decay_t<Callable>; //可調用對象的類型(注意,decay_t去掉其引用、cv等特性)

    callable_t mObj; //以副本的形式保存可調用對象
    args_t mArgs; //以副本的形式保存可調用對象的全部實參。(打包放在tuple中)
public:

    //注意,不論是可調用對象(callableObj),仍是它的實參(args)均會根據其左右值特性,被複制或移動到Binder類中,以副本的形式保存起來。
    explicit binder(Callable&& callableObj, Args&& ... args)
                 :mObj(std::forward<Callable>(callableObj)),mArgs(std::forward<Args>(args)...)
    {
    }

    //Binder仿函數的調用
    template<class... Params>
    decltype(auto) operator()(Params&& ... params) //可調用對象被調用,並傳入參數。
    {
        //第1個參數:升序的整數序列。第4個參數將傳入的params實參轉化爲tuple對象。
        //注意:std::forward_as_tuple被定義成tuple<_Types&&...>(_STD forward<_Types>(_Args)...);
        //      這說明params將以引用的形式被保存在tuple中!!!
        return bind_invoke(Seq{}, mObj, mArgs, std::forward_as_tuple(std::forward<Params>(params)...));
    }
};

//3.1 bind輔助函數
template <class Callable, class... Args>
decltype(auto) bind(Callable&& callableObj, Args&& ... args)
{
    return binder<Callable, Args...>(std::forward<Callable>(callableObj), std::forward<Args>(args)...);
}

//測試函數
int add(int x, int y)
{
    return x + y;
}
//測試類
class Test
{
public:
    int x;
    int add(int x, int y) 
    {
        return x + y;
    }
};

int main()
{  
    // 自定義bind函數的測試
    auto f1 = bind(add, 1, 3); //add爲函數名,會自動轉爲函數指針類型。
    cout << f1() << endl;  //4

    auto f2 = bind(add, _0, _1);
    cout << f2(2, 3) << endl; //5

    auto lam = [](int x, int y) {return x * y; };
    auto f3 = bind(lam, _0, 3);
    cout << f3(3) << endl;  //9

    int a = 10;
    Test t1;
    auto f4 = bind(&Test::add, &t1, _0, _1); //add函數的第1個參數是this指針,所以須要將&t1傳進去。同時注意add是成員函數,
                                             //須要用&來建立指向成員的函數指針。
    cout << f4(a, 3) << endl; //13。 a和3都是以引用的形式傳入「綁定對象」f4中。
    
    t1.x = 100;
    auto f5 = bind(&Test::x, t1);           //傳入t1的副本
    auto f6 = bind(&Test::x, std::ref(t1)); //傳入t1的引用
    
    t1.x = 200;

    cout << f5() << endl;  //100
    cout << f6() << endl;  //200

    return 0;
}
std::bind的模擬實現

1. 保存可調用對象及其形參ide

  (1)保存可調用對象的實例:bind時會將可調用對象(如func)做爲binder類的一個成員變量(如mObj)保存起來。函數

  (2)保存形參:因爲形參是變參,不能直接將變參做爲成員變量。這裏可用tuple將變參打包並保存起來(如mArgs)。測試

  2. 可調用對象的執行優化

  (1)將tuple展開爲變參:綁定可調用對象時,是將可調用對象的形參(可能含佔位符)保存在tuple中。到了調用階段,就必須反過來將tuple展開爲可變參數。由於這個可變參數纔是可調用對象的形參。這裏藉助一個整型序列來將tuple變爲可變參數,在展開tuple的過程當中還須要根據佔位符來選擇合適實參,即佔位符要替換爲調用實參。this

  (2)根據佔位符來選擇合適的實參(如select_param函數)spa

 

     ①tuple中可能含有佔位符,若是發現某個元素類型爲佔位符,則從調用時生成的實參tuple(如params)中取出相應的元素,用來做爲變參的一個參數。如上面的select_param(ph<0>,{4,5,6}),ph<0>是個佔位符,表示該處的實參是其後的{4,5,6}這個tuple中的0位置元素,即4。(具體的實現見do_select_param特化版本)指針

    ②若是某個類型不爲佔位符時,則直接從綁定時生成的形參tuple(如mArgs)中出取參數,用來做爲變參的一個參數。如select_param(1,{4,5,6}),因爲第1個實參爲1,不是佔位符,所以直接將1這個實參取出,傳入invoke函數(具體實現見do_select_param泛化版本)

(二)注意事項

  1.bind函數的全部實參(含第1個實參)都是按值傳遞的,即它們均經過複製或移動的方式以副本的形式保存起來的。能夠經過對實參實施std::ref的方式來達到按引用存儲的效果

  2.bind的返回值(稱之爲「綁定對象」)的全部實參都是按引用傳遞的,由於這些參數被打包成std::forward_as_tuple,而這裏存的都是引用。【見binder的operator()函數】

  3. 對於事先綁定的參數須要傳具體的變量或值進去。對於不事先綁定的參數,須要傳遞佔位符(如_1)進去。

  4. 綁定類的成員函數時,須要用&來建立成員函數指針再將其做爲實參傳給bind的第1個實參,同時在bind的第2個實參中爲該成員函數指定this指針,而後再傳入該成員函數的其它參數。

二. 優先使用lambda而非bind

(一)lambda表達式具備更強的可讀性,表達力更好。

(二)bind函數綁定重載函數時會遇到二義性問題。因爲函數名只是個名字,不帶形參類型等其餘附加信息,因此沒法知道被綁定的是哪一個重載版本的函數。

(三)lambda表達式可能生成比使用std::bind運行效率更高的代碼。編譯器可能對函數名作inline函數調用,而不太可能對函數指針作這種優化。如setAlarm函數在lambda中的調用,能夠被內聯。可是std::bind綁定的是setAlarm函數指針而不是函數體自己,因此沒法被內聯

(四)lambda的傳參方式(按值仍是按引用)比bind函數更直觀Lambda表達式的傳參方式只需看其聲明就可知。但使用bind須要牢記其工做原理,即bind在綁定時是按值傳遞實參的,綁定對象調用時是按引用傳遞實參的

(五)自C++14之後,std::bind己經完全失敗用武之地,能夠徹底被lambda替代。

3、bind使用的兩種場合(C++11中)

(一)移動捕獲:C++11中的lambda沒有提供移動捕獲特性,但能夠經過std::bind和lambda表達式來模擬移動捕獲(見《第18課 捕獲機制及陷阱》)。而C++14中提供了初始化捕獲的語言特性,從而消除了如此模擬的必要。

(二)多態函數對象:由於綁定對象的函數調用運算符利用了完美轉發,它就能夠接受任何類型的實參。當想要綁定的對象具備一個函數運算符模板時,是有利用價值的。但在C++14中,使用帶有auto類型形參的lambda能夠垂手可得地達成一樣的效果。

【編程實驗】lambda與bind的pk

#include <iostream>
#include <functional>
#include <chrono>

using namespace std;
using namespace std::chrono;   //for steady_clock
using namespace std::literals; //for 1h, 30s等
using namespace placeholders;  //for _一、_2等。

using Time = std::chrono::steady_clock::time_point; //表示時刻的類型
using Duration = std::chrono::steady_clock::duration; //表示時長的類型
enum class Sound{Beep, Siren, Whistle}; //警報的聲音類型(嗶嗶聲、汽笛聲、口哨聲)
enum class Volume{Normal, Loud, LoudPlusPlus}; //音量大小

//警報函數(3個形參)
void setAlarm(Time t, Sound s, Duration d) //在t時刻,發出聲音s,持續時長d
{
}

//警報函數(4個形參)
void setAlarm(Time t, Sound s, Duration d, Volume v)
{
}

class Widget{};
enum class CompLevel{Low, Normal, High}; //壓縮等級
Widget compress(const Widget& w, CompLevel lev)  //製做w的壓縮副本
{
    return Widget();
}

class PolyWidget
{
public:
    template<typename T>
    void operator()(const T& param){} //仿函數可接受任何類型的形參對象,被稱爲多態函數對象!
};

int main()
{
    //1. lambda比bind可讀性更強(下列between函數用於判斷實參是否介於極小值和極大值之間)
    const auto lowVal = 0;
    const auto highVal = 100;
    //1.1使用lambda
    auto betweenL = [lowVal, highVal](const auto& val)  //C++14
    {
        return lowVal <= val && val <= highVal; 
    };
    
    //1.2 使用bind
    auto betweenB = std::bind(std::logical_and<>(),
                              std::bind(std::less_equal<>(), lowVal, _1),
                              std::bind(std::less_equal<>(), _1, highVal));

    int x = 15, y = 150;
    cout << betweenL(x) << endl; //1
    cout << betweenB(x) << endl; //1
    cout << betweenL(y) << endl; //0
    cout << betweenB(y) << endl; //0

    //2. bind在延時求值、識別重載函數的使用方式繁瑣。此外綁定函數時不會被內聯到operator()中
    //2.1 lambda表達式(C++14)
    auto setSoundL = [](Sound s)  //setSoundL後面的L表達lambda表達式
    {
        setAlarm(steady_clock::now() + 1h, s, 30s); //C++14支持1h、30s的寫法。調用3個形參的setAlarm,不會發生二義性!
                                                    //因爲setAlarm是常規的函數調用,編譯器會考慮將setAlarm內聯到lambda中
    };
    setSoundL(Sound::Beep);

    //2.2 使用bind
    //2.2.1 錯誤的用法(警報啓動時刻是在調用std::bind函數以後的1個小時)
    //auto setSoundB = std::bind(setAlarm,             //setSoundB中的"B"表示bind
    //                     steady_clock::now() + 1h,   //錯誤!該參數傳給bind,而非setAlarm。意味着調用bind時時刻值己求出。但咱們指望是setAlarm調用時對時刻求值。
    //                     _1, //將傳給setSoundB的第1個參數傳到setAlarm的第2個參數中。此處bind沒法知道該實參真正的類型,需向setAlaram查詢!
    //                       30s); 
    //2.2.2 正確的用法(警報啓動時刻是在調用setAlarm函數以後的1個小時)
    using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);
    auto setSoundB = std::bind(static_cast<SetAlarm3ParamType>(setAlarm),   //setAlarm是個函數名。沒有形參信息,不知調用哪一個重載版本的setAlarm,需強轉!
                               std::bind(std::plus<>(), std::bind(steady_clock::now), 1h), //延時求值,注意這裏傳入的是now這個函數,而不是now()的返回值!
                               _1,
                               30s
                              );
    setSoundB(Sound::Beep);  //因爲bind參數中的setAlarm是個函數指針,故setSoundB的operator()中沒法將setAlarm函數體自己內聯進來。

    //3. bind的傳參方式不直觀
    Widget w;
    //3.1 在bind調用時的傳參比較
    auto compressRateB = std::bind(compress, w, _1); //bind時w被按值傳遞給綁定對象,是以副本形式被存儲。(但這一點從bind的形參中看不出來,只有牢記其工
                                                     //做原理才能知道這個事實。

    auto compressRateL = [w](CompLevel lev) {return compress(w, lev); }; //使用lambda時很是直觀,w和lev都是以按值方式被傳遞給lambda表達式。

    //3.2 在綁定對象被調用時傳參的比較
    compressRateB(CompLevel::High); //bind返回的綁定對象調用時,實參都是經過按引用傳遞的(這點也是要牢記bind工做原理纔會知道的事實)
    compressRateL(CompLevel::High); //參數是以按值方式傳遞的。

    //4. 多態函數對象
    PolyWidget pw;
    //4.1 C++11中,bind的用途。綁定多態仿函數對象
    auto pwB = std::bind(pw, _1);
    pwB(1930); //傳入int
    pwB(nullptr); //傳入nullptr
    pwB("SantaClaus"); //傳入字符串字面量,pw.operator()中能夠傳入各類類型的對象。

    //4.2 C++14中,可利用帶auto形參的lambda來達到相似的效果
    auto pwL = [&pw](const auto& param)
    {
        pw(param);
    };

    pwL(1930); //傳入int
    pwL(nullptr); //傳入nullptr
    pwL("SantaClaus"); //傳入字符串字面量,pw.operator()中能夠傳入各類類型的對象。

    return 0;
}
相關文章
相關標籤/搜索