1. 程式人生 > >Effective Modern C++:06lambda表示式

Effective Modern C++:06lambda表示式

         lambda表示式實際上是語法糖,任何lambda表示式能做到的,手動都能做到,無非是多打幾個字。但是lambda作為一種建立函式物件的手段,實在太過方便,自從有了lambda表示式,使用複雜謂詞來呼叫STL中的”_if”族演算法(std::find_if,std::remove_if等)變得非常方便,這種情況同樣發生在比較函式的演算法族上。在標準庫之外,lambda表示式可以臨時製作出回撥函式、介面適配函式或是語境相關函式的特化版本以供一次性呼叫。下面是關於lambda相關術語的提醒:

         lambda表示式,是表示式的一種,比如下面程式碼中紅色的就是lambda表示式:

std::find_if(container.begin(), container.end(),
[](int val) { return 0 < val && val < 10; });

          閉包,是lambda表示式建立的執行期物件,在上面對std::find_if的呼叫中,閉包就是作為第三個實參在執行期傳遞給std::find_if的物件。

         閉包類,是例項化閉包的類,每個lambda表示式都會觸發編譯器生成一個獨一無二的閉包類,而lambda表示式中的語句會變成閉包類成員函式的可執行指令。

        

閉包可以複製,所以,對應於單獨一個lambda表示式的閉包型別可以由多個閉包:

int x; // x is local variable
auto c1 = [x](int y) { return x * y > 55; }; // c1 is copy of the closure produced by the lambda
auto c2 = c1; // c2 is copy of c1
auto c3 = c2; // c3 is copy of c2

          c1、c2和c3都是同一個lambda表示式產生的閉包的副本。

         在非正式場合,lambda表示式,閉包和閉包類之間的界限可以模糊一些。但是在下面的條款中,需要能區別哪些存在於編譯期(lambda表示式和閉包類),哪些存在於執行期(閉包),以及它們之間的相互聯絡。

 

31:避免預設捕獲模式

         C++11中有兩種預設捕獲模式:按引用或按值。按引用的預設捕獲模式可能導致空懸引用,而按值的預設捕獲模式可能會讓你覺得不存在空懸引用的問題(實際上不是)。

         按引用捕獲會導致閉包包含指向區域性變數(或形參)的引用,一旦由lambda表示式所建立的閉包的生命期超過了該區域性變數或形參的生命期,那麼閉包內的引用就會空懸,比如下面的程式碼:

using FilterContainer = std::vector<std::function<bool(int)>>; 
FilterContainer filters; // filtering funcs

void addDivisorFilter()
{
    auto calc1 = computeSomeValue1();
    auto calc2 = computeSomeValue2();
    auto divisor = computeDivisor(calc1, calc2);
    filters.emplace_back(
        [&](int value) { return value % divisor == 0; }
    ); 
}

          這段程式碼隨時會出錯,lambda中按引用捕獲了局部變數divisor,但當addDivisorFilter函式返回時區域性變數被銷燬,使用filters就會產生未定義行為。

 

         如果不這樣做,使用顯式方式按引用捕獲divisor,問題依然存在:

filters.emplace_back(
    [&divisor](int value) { return value % divisor == 0; } 
);

          但是通過顯示捕獲,就比較容易看出lambda表示式的生存依賴於divisor的生命期。顯式的寫出”divisor”可以提醒我們要保證divisor至少應該和lambda具有一樣長的生命期,這要比[&]這種所傳達的不痛不癢的“要保證沒有空懸引用”式的勸告更讓人印象深刻。

 

         如果知道閉包會立即使用(比如傳遞給STL演算法)且不會被複制,這種情況下,你可能會爭論說,既然沒有空懸引用的風險,也就沒有必要避免使用預設引用捕獲模式。但是從長遠觀點來看,顯示的列出lambda表示式所依賴的區域性變數或形參,是更好的軟體工程實踐。

 

         上面的例子中,解決問題的一種辦法是對divisor採用按值的預設捕獲模式:

filters.emplace_back(
    [=](int value) { return value % divisor == 0; }
);

          對於這個例子而言,這樣做確實是沒問題的。但是按值的預設捕獲並非一定能避免空懸引用,問題在於如果按值捕獲了一個指標,在lambda表示式建立的閉包中持有的是這個指標的副本,但是沒有辦法阻止lambda表示式之外的程式碼針對該指標實施delete操作導致的指標副本空懸。比如下面的程式碼:

class Widget {
public:
    … // ctors, etc.
    void addFilter() const; // add an entry to filters
private:
    int divisor; // used in Widget's filter
};

void Widget::addFilter() const {
    filters.emplace_back(
        [=](int value) { return value % divisor == 0; }
    );
}

          這樣的程式碼看起來安全,然而實際上卻是大錯特錯的。捕獲只能針對於在建立lambda表示式的作用域內可見的非靜態區域性變數(包括形參),而在Widget::addFilter函式體內,divisor並非區域性變數,而是Widget類的成員變數,它根本沒辦法捕獲。這麼一來,如果不使用預設捕獲模式,程式碼就不會通過編譯:

void Widget::addFilter() const {
    filters.emplace_back(
        [](int value) { return value % divisor == 0; }
    );
}

          而且,如果試圖顯示捕獲divisor(無論是按值還是按引用),這個捕獲語句都不能通過編譯,因為divisor既不是區域性變數,也不是形參:

void Widget::addFilter() const {
    filters.emplace_back(
        [divisor](int value) { return value % divisor == 0; }
    );
}

          但是為什麼一開始的程式碼沒有發生編譯錯誤呢?this指標是關鍵所在,每一個非靜態成員函式都持有一個this指標,每當提及該類的成員變數時都會用到這個指標。比如在Widget的任何成員函式中,編譯器內部都會把divisor替換成this->divisor。因此,在Widget::addFilter的按值預設捕獲版本中,被捕獲的實際上是Widget的this指標,而不是divisor。從編譯器的角度來看,實際的程式碼相當於:

void Widget::addFilter() const {
    auto currentObjectPtr = this;
    filters.emplace_back(
        [currentObjectPtr](int value) { return value % currentObjectPtr->divisor == 0; }
    );
}

          因此,該lambda閉包的存活,與它含有this指標指向的Widget物件的生命期是綁在一起的,比如下面的程式碼:

using FilterContainer = std::vector<std::function<bool(int)>>;
FilterContainer filters; 

void doSomeWork() {
    auto pw = std::make_unique<Widget>(); 
    pw->addFilter();
    …
}

          當呼叫doSomeWork時建立了一個篩選函式,它依賴於std::make_unique建立的Widget物件,該函式被新增到filters中,然而當doSomeWork結束後,Widget物件隨著std::unique_ptr的銷燬而銷燬,從那一刻起,filters中就含有了一個帶有空懸指標的元素。

         這一問題可以通過將想捕獲的成員變數複製到區域性變數中,而後捕獲該區域性變數的部分得意解決:

void Widget::addFilter() const {
    auto divisorCopy = divisor;
    filters.emplace_back(
        [divisorCopy](int value) { return value % divisorCopy == 0; } 
    );
}

  

         在C++14中,捕獲成員變數的一種更好的方式是使用廣義lambda捕獲(generalized lambda):

void Widget::addFilter() const {
    filters.emplace_back(
        [divisor = divisor](int value) { return value % divisor == 0; }
    );
}

          對廣義lambda捕獲而言,沒有預設捕獲模式一說,但是,就算在C++14中,本條款的建議,避免使用預設捕獲模式依然成立。

 

         使用按值預設捕獲的另一個缺點,在於它似乎表明閉包是自治的,與閉包外的資料變化絕緣,然而作為一般性的結論,這是不正確的。因為lambda表示式可能不僅依賴於區域性變數或形參,他還可能依賴於靜態儲存期物件,這樣的物件定義在全域性或名字空間作用域中,或是在類,函式,檔案中以static飾詞宣告。這樣的物件可以在lambda內使用,但是它們不能被捕獲。如果使用了按值預設捕獲模式,這些物件就會給人以錯覺,認為它們可以加以捕獲:

void addDivisorFilter() {
    static auto calc1 = computeSomeValue1(); 
    static auto calc2 = computeSomeValue2(); 
    static auto divisor = computeDivisor(calc1, calc2);
    filters.emplace_back(
        [=](int value) { return value % divisor == 0; }
    );
    ++divisor;
}

          看到[=]就認為lambda複製了它內部使用的物件,得出lambda是自治的這種結論,是錯誤的。實際上該lambda表示式並不獨立,它沒有使用任何的非靜態區域性變數或形參,所以它沒能捕獲任何東西。更糟糕的是lambda表示式的程式碼中使用了靜態變數divisor,每次呼叫addDivisorFilter後,divisor會遞增,使得新增到filters中的每個lambda表示式的行為都不一樣。如果一開始就避免使用按值的預設捕獲模式,也就能消除程式碼被誤讀的風險了。

 

 

32:能夠初始化捕獲將物件移入閉包

         有時按值捕獲和按引用捕獲並不能滿足所有的需求。比如想要把move-only物件(如std::unique_ptr或std::future)放入閉包,或者想把複製昂貴而移動低廉的物件移入閉包時,C++11沒有提供可行的方法,但是C++14為物件移動提供了直接支援。

         實際上,C++14提供了一種全新的捕獲方式,按移動的捕獲只不過是該機制能夠實現的多種效果之一罷了。這種方式稱為初始化捕獲(init capture),它可以做到C++11的捕獲形式所有能夠做到的事情(除了預設捕獲模式,而這是需要遠離的),不過初始化捕獲的語法稍顯囉嗦,如果C++11的捕獲能解決問題,則大可以使用之。

         下面是初始化捕獲實現移入捕獲的例子:

class Widget { 
public:
    bool isValidated() const;
    bool isProcessed() const;
    bool isArchived() const;
private:
    …
};
auto pw = std::make_unique<Widget>(); 
… 
auto func = [pw = std::move(pw)] 
            { return pw->isValidated() && pw->isArchived(); };

          上面的例子中,位於”=”左側的pw,是lambda建立的閉包類中成員變數的名字;而位於”=”右側的是其初始化表示式,所以”pw=std::move(pw)”表達了在閉包類中建立一個成員變數pw,然後使用針對區域性變數pw實施std::move的結果來初始化該成員變數。在lambda內部使用pw也是指的閉包類的成員變數。一旦定義lambda表示式之後,因為區域性變數pw已經被move了,所以其不再掌握任何資源。

         上面的例子還可以不使用區域性變數pw:

auto func = [pw = std::make_unique<Widget>()]
            { return pw->isValidated() && pw->isArchived(); };

          這種捕獲方式在C++14中還稱為廣義lambda捕獲(generalized lambda capture)。

 

         但是如果編譯器尚不支援C++14,則該如何實現按移動捕獲呢?要知道一個lambda表示式不過是生成一個類並建立一個該類的物件的手法罷了,並不存在lambda能做而手工不能做的事情,上面C++14的例子,如果使用C++11,可以寫為:

class IsValAndArch { 
public: 
    using DataType = std::unique_ptr<Widget>;
    explicit IsValAndArch(DataType&& ptr) : pw(std::move(ptr)) {}
    bool operator()() const
    { return pw->isValidated() && pw->isArchived(); }
private:
    DataType pw;
};
auto func = IsValAndArch(std::make_unique<Widget>());

          這種寫法要比使用lambda麻煩很多。

如果非要使用lambda實現按移動捕獲,也不是全無辦法,可以藉助std::bind實現:把要捕獲的物件移動到std::bind產生的函式物件中;給lambda表示式一個指向欲捕獲的物件的引用。比如C++14中的寫法:

std::vector<double> data;  
… // populate data
auto func = [data = std::move(data)]
            { /* uses of data */ };

          如果採用C++11中使用std::bind和lambda的寫法,等價程式碼如下:

std::vector<double> data; 
… // as above
auto func = std::bind(
    [](const std::vector<double>& data) { /* uses of data */ },
    std::move(data)
);

          std::bind也生成函式物件,可以將它生成的物件稱為繫結物件。std::bind的第一個實參是個可呼叫物件,接下來的所有實參表示傳遞給該物件的值。

         繫結物件內含有傳遞給std::bind所有實參的副本。對於左值實參,繫結物件內對應的副本實施的是複製構造;對於右值實參,實施的是移動構造。上面的例子中,第二個實參是個右值,所以在繫結物件內,使用區域性變數data移動構造其副本,這種移動構造動作正是實現模擬移動捕獲的關鍵所在,因為把右值移入繫結物件,正是繞過C++11無法將右值移動到閉包的手法。

         當一個繫結物件被呼叫時,它所儲存的實參會傳遞給std::bind的那個可呼叫物件,也就是func被呼叫時,func內經由移動構造得到的data副本就會作為實參傳遞給那個原先傳遞給std::bind的lambda表示式。這個C++11寫法比C++14多了一個形參data,該形參是個指向繫結物件內部的data副本的左值引用,這麼一來,在lambda內對data形參所做的操作,都會實施在繫結物件內移動構造而得的data副本之上,與原區域性變數data無關。

 

         預設情況下,lambda閉包類中的operator()成員函式會帶有const飾詞,因此閉包裡的所有成員變數在lambda表示式的函式體內都帶有const飾詞,但繫結物件內移動構造而得的data副本並不帶有const飾詞,所以為了防止該data部分在lambda表示式內被意外修改,lambda的形參就宣告為常量引用。但是如果lambda表示式帶有mutable飾詞,則閉包中的operator()函式就不會在宣告時帶有const飾詞,相應的做法就是在lambda宣告中略去const:

auto func = std::bind(
    [](std::vector<double>& data) mutable 
    { /* uses of data */ },
    std::move(data)
);

          繫結物件儲存著傳遞給std::bind所有實參的副本,因此本例中的繫結物件就包含一份由第一個實參lambda表示式產生的閉包的副本。這麼一來,該閉包的生命期就和繫結物件是相同的。

 

         另外一個例子,下面是C++14的程式碼:

auto func = [pw = std::make_unique<Widget>()] 
            { return pw->isValidated() && pw->isArchived(); }; 

          如果使用C++11採用bind的寫法:

auto func = std::bind(
    [](const std::unique_ptr<Widget>& pw)
    { return pw->isValidated() && pw->isArchived(); },
    std::make_unique<Widget>()
);

  

 

33:要對auto&&型別的形參使用std::forward,則需要使用decltype

         泛型lambda表示式(generic lambda)是C++14最振奮人心的特性之一:lambda表示式的形參列表中可以使用auto,它的實現直截了當,閉包類中的operator()採用模板實現。比如下面的lambda表示式,以及其對應的實現:

auto f = [](auto x){ return func(normalize(x)); };

class SomeCompilerGeneratedClassName {
public:
    template<typename T> 
    auto operator()(T x) const
    { return func(normalize(x)); }
    …
}; 

          這個例子中,lambda表示式對x的動作就是將其轉發給normalize,如果normalize區別對待左值和右值,則該lambda表示式的實現是有問題的,正確的寫法應該是使用萬能引用並將其完美轉發給normalize:

auto f = [](auto&& x)
{ return func(normalize(std::forward<???>(x))); };

          這裡的問題是,std::forward的模板實參”???”應該怎麼寫?

這裡可以使用decltype(x),但是decltype(x)產生的結果,卻於std::forward的使用慣例有所不同。如果傳入的是個左值,則x的型別是左值引用,decltype(x)得到的也是左值引用;如果傳入的是右值,則x的型別是右值引用,decltype(x)得到的也是右值引用,但是,std::forward的使用慣例是std::forward<T>,其中T要麼是個左值引用,要麼是個非引用。

         再看一下條款28中std::forward的簡單實現:

template<typename T> 
T&& forward(remove_reference_t<T>& param) {
    return static_cast<T&&>(param);
}

          如果客戶程式碼想要完美轉發Widget型別的右值,則按照慣例它應該採用Wdiget型別,而非引用型別來例項化std::forward,然後std::forard模板例項化結果是:

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

          如果使用右值引用例項化T,也就是Widget&&例項化T,得到的結果是:

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

          實施了引用摺疊之後:

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

          經過對比,發現這個版本和T為Widget時的std::forward是完全一樣的,因此,例項化std::forward時,使用一個右值引用和使用非引用型別,結果是相同的。所以,我們的完美轉發lambda表示式如下:

auto f =
    [](auto&& param)
    {
        return func(normalize(std::forward<decltype(param)>(param)));
    };

          稍加改動,就可以得到能接收多個形參的完美轉發lambda式版本,因為C++14中的lambda能夠接受變長形參:

auto f =
    [](auto&&... params)
    {
        return func(normalize(std::forward<decltype(params)>(params)...));
    };

 

  

34:優先使用lambda表示式,而非std::bind

         std::bind在2005年就已經是標準庫的組成部分了(std::tr1::bind),這意味著std::bind已經存在了十多年了,你可能不太願意放棄這麼一個運作良好的工具,然而有時候改變也是有益的,因為在C++11中,相對於std::bind,lambda幾乎總會是更好的選擇,而到了C++14,lambda簡直已成了不二之選。

         lambda表示式相對於std::bind的優勢,最主要的是其具備更高的可讀性:

// typedef for a point in time (see Item 9 for syntax)
using Time = std::chrono::steady_clock::time_point;
// see Item 10 for "enum class"
enum class Sound { Beep, Siren, Whistle };
// typedef for a length of time
using Duration = std::chrono::steady_clock::duration;
// at time t, make sound s for duration d
void setAlarm(Time t, Sound s, Duration d);

// setSoundL ("L" for "lambda") is a function object allowing a
// sound to be specified for a 30-sec alarm to go off an hour
// after it's set
auto setSoundL = [](Sound s)
    {
        // make std::chrono components available w/o qualification
        using namespace std::chrono;
        setAlarm(steady_clock::now() + hours(1), s, seconds(30));
    };

          這裡的lambda表示式,即使是沒什麼經驗的讀者也能看出來,傳遞給lambda的形參會作為實參傳遞給setAlarm。到了C++14中,C++14提供了秒,毫秒和小時的標準字面值,所以,可以寫成這樣:

auto setSoundL =
[](Sound s)
{
    using namespace std::chrono;
    using namespace std::literals;
    setAlarm(steady_clock::now() + 1h, s, 30s);
};

          而下面的程式碼是使用std::bind的等價版本,不過實際上它還有一處錯誤的,後續在解決這個錯誤:

using namespace std::chrono;
using namespace std::literals;
using namespace std::placeholders; // needed for use of "_1"
auto setSoundB = // "B" for "bind"
    std::bind(setAlarm, steady_clock::now() + 1h, _1, 30s);

          對於初學者而言,佔位符”_1”簡直好比天書,而即使是行家也需要腦補出從佔位符數字到它在std::bind形參列表中的位置對映關係,才能理解在呼叫setSoundB時傳入的第一個實參,會作為第二個實參傳遞給setAlarm。該實參的型別在std::bind的呼叫過程中是未加識別的,所以還需要檢視setAlarm的宣告才能決定應該傳遞何種型別的實參到setSoundB。

         這段程式碼的錯誤之處在於,在lambda表示式中,表示式”steady_clock::now() + 1h”是setAlarm的實參之一,這一點清清楚楚,該表示式會在setAlarm被呼叫時求值,這樣是符合需求的,就是需要在setAlarm被呼叫的時刻之後的一個小時啟動報警。但是在std::bind中,”steady_clock::now() + 1h”作為實參傳遞給std::bind,而非setAlarm,該表示式在呼叫std::bind時就進行求值了,並且求得的結果會儲存在繫結物件中,這導致的結果是報警的啟動時刻是在std::bind呼叫之後的一個小時,而非setAlarm呼叫之後的一個小時。

         要解決這個問題,就需要std::bind延遲表示式的求值到呼叫setAlarm的時刻,實現這一點,就是需要巢狀第二層std::bind的呼叫:

auto setSoundB =
    std::bind(setAlarm,
              std::bind(std::plus<>(), steady_clock::now(), 1h),
              _1,
              30s);

          在C++14中,標準運算子模板的模板型別實參大多數情況下可以省略不寫,所以此處也沒必要在std::plus中提供了,而C++11中還沒有這樣的特性,所以在C++11中,想要實現上面的程式碼,只能是:

using namespace std::chrono; // as above
using namespace std::placeholders;
auto setSoundB =
    std::bind(setAlarm,
         std::bind(std::plus<steady_clock::time_point>(), steady_clock::now(), hours(1)),
         _1,
         seconds(30));

  

         如果對setAlarm實施了過載,則又會有新的問題:

enum class Volume { Normal, Loud, LoudPlusPlus };
void setAlarm(Time t, Sound s, Duration d, Volume v);
auto setSoundL = 
[](Sound s)
{
    using namespace std::chrono;
    setAlarm(steady_clock::now() + 1h, s, 30s);
};

          即使有了過載,lambda表示式依然能正常工作,過載決議會選擇有三個引數版本的setAlarm。但是到了std::bind,就沒辦法通過編譯了:

auto setSoundB = 
    std::bind(setAlarm, 
            std::bind(std::plus<>(),steady_clock::now(),1h),
            _1,
            30s);

          這是因為編譯器無法確定應該將哪個setalarm傳遞給set::bind,它拿到的所有資訊只有一個函式名。為了使std::bind能夠通過編譯,setAlarm必須強制轉換到適當的函式指標型別:

using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);
auto setSoundB = 
    std::bind(static_cast<SetAlarm3ParamType>(setAlarm),
            std::bind(std::plus<>(), steady_clock::now(), 1h),
            _1,
            30s);

          但是這麼做又帶來了lambda和std::bind的另一個不同之處。在lambda生成的setSoundL的函式呼叫運算子中,呼叫setAlarm採用的是常規函式喚起方式,這麼一來,編譯器就可以用慣常的手法將其內聯:

setSoundL(Sound::Siren); // body of setAlarm may well be inlined here

          而std::bind呼叫中使用了函式指標,這意味著在setSoundB的函式呼叫運算子中,setAlarm是通過函式指標來呼叫的,編譯器一般無法將函式指標發起的函式呼叫進行內聯,所以lambda表示式就有可能生成比std::bind更快的程式碼。

 

         在setAlarm例子中,僅僅涉及了函式的呼叫而已,如果你想做的事比這更復雜,則lambda表示式的優勢則更加明顯。比如:

auto betweenL =
    [lowVal, highVal]
    (const auto& val)
    { return lowVal <= val && val <= highVal; };

          這裡的lambda使用了捕獲。std::bind要想要實現同樣的功能,必須用比較晦澀的方式來構造程式碼,下面分別是C++14和C++11的寫法:

using namespace std::placeholders;
auto betweenB =
    std::bind(std::logical_and<>(),
        std::bind(std::less_equal<>(), lowVal, _1),
        std::bind(std::less_equal<>(), _1, highVal));

auto betweenB = 
    std::bind(std::logical_and<bool>(),
        std::bind(std::less_equal<int>(), lowVal, _1),
        std::bind(std::less_equal<int>(), _1, highVal));

          還是需要使用std::bind的延遲計算方法。

        

         再看下面的程式碼:

enum class CompLevel { Low, Normal, High }; 
Widget compress(const Widget& w, CompLevel lev); //make compressedcopy of w
Widget w;
using namespace std::placeholders;
auto compressRateB = std::bind(compress, w, _1);

          這裡的w傳遞給std::bind時,是按值儲存在std::bind生成的物件中的,在std::bind的呼叫中,按值還是按引用儲存只能是牢記規則。std::bind總是複製其實參,但是呼叫方可以通過對實參實施std::ref的方法達到按引用儲存的效果,因此:

auto compressRateB = std::bind(compress, std::ref(w), _1);

 結果就是compressRateB的行為如同持有的是個指向w的引用,而非其副本。

而在lambda中,w無論是按值還是按引用捕獲,程式碼中的書寫方式都很明顯:

auto compressRateL = 
[w](CompLevel lev)
{ return compress(w, lev); };

          同樣明顯的還有形參的傳遞方式:

compressRateL(CompLevel::High); // arg is passed by value
compressRateB(CompLevel::High); // how is arg passed?

          Lambda返回的閉包中,很明顯實參是按值傳遞給lev的;而在std::bind返回繫結物件中,形參的傳遞方式是什麼呢?這裡也只能牢記規則,繫結物件的所有實參都是按引用傳遞的,因為此種物件的函式呼叫運算子使用了完美轉發。

        

         總而言之,lambda表示式要比std::bind可讀性更好,表達能力更強,執行效率也可能更好,在C++14中,幾乎沒有std::bind的適當用例,而在C++11中,std::bind僅在兩個受限場合還算有使用的理由:

         移動捕獲,C++11沒有提供移動捕獲的語法,參考上一條款;

多型函式物件,因為繫結物件的函式呼叫運算子使用了完美轉發,所以可以接收任何型別的實參,因此當需要繫結的物件具有一個函式呼叫運算子模板時,是有利用價值的:

class PolyWidget {
public:
    template<typename T>
    void operator()(const T& param);
    …
};

PolyWidget pw;
auto boundPW = std::bind(pw, _1);
boundPW(1930); // pass int to PolyWidget::operator()
boundPW(nullptr); // pass nullptr to PolyWidget::operator()
boundPW("Rosebud"); // pass string literal to PolyWidget::operator()

          C++11中的lambda表示式沒有辦法實現這一點,但是在C++14中,使用帶有auto型別形參的lambda表示式可以很容易的實現這一點:

auto boundPW = 
[pw](const auto& param) 
{ pw(param); };