在文章《C++ 學習筆記》中, 我們已經詳細講述了 lambda 表達式. 在文章《【C++ Paper 與 Proposal 導讀】Lambda 合集》中, 我們又將 lambda 表達式延伸到了 C++ 20, 並且解釋了在 C++ 20 之前, 一些 lambda 使用不太便利的地方和需要注意的地方 (絕大多數不便利的地方在 C++ 20 中已經被新的特性所解決). 這篇文章針對 lambda 表達式作一些補充

1. lambda 表達式的捕獲列表並不會捕獲類別中的非靜態成員變數

class Foo {
    constexpr static auto value {0};
    int x;
public:
    void func() {
        auto lambda {[x]() mutable {        //Compile Error : 'x' in capture list does not name a variable
            return x + 1;
        }};
    }
};

你可能認為下面的捕獲會將靜態成員也包含進來 :

class Foo {
    constexpr static auto value {0};
    int x;
public:
    void func() {
        auto lambda {[=]() mutable {
            return x + value + 1;
        }};
    }
};

但是, value 是靜態成員, 它本身在 lambda 表達式之內就是可見的, 因此它並不是被捕獲的

2. 優先選用 lambda 表達式, 而不是 std::bind

現在考慮一個函式, 其功能為每隔一段時間呼叫某個函式 :

class time;
class countdown;

template <typename F>
void caller(class time start, countdown seconds, F f);

其中, 類別 time 是精確到秒的時間型別, 它包含了一個回傳目前時間的靜態成員函式 now; countdown 是倒計時計數器的型別. 現在, 我們為用戶提供一個從目前開始, 倒計時為 1 分鐘的內建函式. 在 C++ 14 之後, 我們有兩個簡便的方法 :

#include <functional>

class time;
class countdown;

template <typename F>
void caller(class time start, countdown seconds, F f);

auto one_minute_lambda {[](auto f) {
    caller(time::now(), 60, f);
}};

auto one_minute_bind {std::bind(caller, time::now(), 60, std::placeholders::_1)};

使用 lambda 表達式的版本 one_minute_lambda 運作地不錯, 但是使用 std::bind 版本的 one_minute_bind 並不是這樣. 我們希望函式 caller 得到的時間是 one_minute_bind 被呼叫的時間. 但是實際上, 在 one_minute_bind 初始化的時候, time::now() 已經運作, 並且將其回傳值繫結到了函式 caller 之上. 要修正這個錯誤, 只能通過延後靜態成員函式 time::now 的呼叫 :

auto one_minute_bind {std::bind(caller, std::bind(time::now), 60, std::placeholders::_1)};

這個版本遠比 lambda 表達式的版本複雜得多, 並且對 C++ 新人並不友好. 除此之外, 一旦函式 caller 存在多載的版本, 那麼編碼器就無法進行選擇. 由於 caller 是一個函式樣板, 我們也沒有辦法通過明確其型別的方式使用 std::bind. 除非 F 是一個固定的型別 :

#include <functional>

class time;
class countdown;

void caller(class time start, countdown seconds, void (*fp)(int));

auto one_minute_lambda {[](auto f) {
    caller(time::now(), 60, f);
}};

auto one_minute_bind {std::bind(static_cast<void (*)(class time, countdown, void (*)(int))>(caller), std::bind(time::now), 60, std::placeholders::_1)};

另外, 我們在《C++ 學習筆記》中提到, 每一個 lambda 表達式都是隱含的 inline 函式, 因此我們呼叫 one_minute_lambda 這個可以呼叫物件的時候, 編碼器很可能幫助我們對 one_minute_lambda 內嵌, 但是對於 std::bind 回傳的物件就不一定了, 而且大概不會被內嵌

std::bind 函式在向其傳遞引數的時候, 都是按值語意傳遞的. 猜想一下下面程式碼最終的輸出是什麼 :

#include <iostream>

void func(int &i) {
    ++i;
}

using namespace std;
int main(int argc, char *argv[]) {
    auto level {0};
    auto callable_obj {std::bind(func, level)};
    callable_obj();
    cout << level << endl;
}

答案是 0. 傳遞給 funci 並不是 level, 而是 std::bind 內部所產生的一個臨時值. 我會在《C++ Standard Template Library》系列的文章中詳細講述 std::bind 的實作, 到時候大家就可以從內部了解 std::bind 是如何運作的. 但是現在, 你只要知道, std::bind 在繫結引數的時候, 並非按照參考繫結. 那麼正確的實作應該是 :

#include <iostream>

void func(int &i) {
    ++i;
}

using namespace std;
int main(int argc, char *argv[]) {
    auto level {0};
    auto callable_obj {[&level]() {
        func(level);
    }};
    callable_obj();
    cout << level << endl;      //輸出結果 : 1
}

不過, 如果非要使用 std::bind, C++ 標準樣板程式庫中還提供了 std::ref 使得 std::bind 可以按照參考進行繫結 :

#include <iostream>

void func(int &i) {
    ++i;
}

using namespace std;
int main(int argc, char *argv[]) {
    auto level {0};
    auto callable_obj {std::bind(func, std::ref(level))};
    callable_obj();
    cout << level << endl;      //輸出結果 : 1
}

在 C++ 14 之後, std::bind 可以做的 lambda 表達式都可以做, 但是在 C++ 11 中, 有兩個地方是 lambda 表達式無法直接完成但是 std::bind 可以直接完成的. 一個是移動捕獲 :

#include <vector>

using namespace std;
int main(int argc, char *argv[]) {
    vector<int> vec {1, 2, 3, 4, 5};
    auto callable {bind([](vector<int> &&vec) {
        //...
    }, move(vec))};
}

在 C++ 14 中, 可以直接使用廣義 lambda 表達式捕獲列表來簡化. 另外一個是在 C++ 11 中模擬泛型 lambda 表達式 :

#include <iostream>

using namespace std;
struct generic_lambda_helper {
    template <typename T>
    void operator()(T &&value) {
        cout << typeid(value).name() << endl;
    }
};
int main(int argc, char *argv[]) {
    generic_lambda_helper helper;
    auto generic_lambda {bind(helper, placeholders::_1)};
    generic_lambda(1);      //輸出結果 (Apple Clang) : i
    generic_lambda(1.1f);       //輸出結果 (Apple Clang) : f
    generic_lambda('a');        //輸出結果 (Apple Clang) : c
}

不過繫結到一個可變參數樣板的函式上, std::bind 最多支援 10 個引數, 因為 C++ 標準樣板程式庫中只提供了 10 個佔位符號 :

#include <iostream>

using namespace std;
struct generic_lambda_helper {
    template <typename ...Args>
    void operator()(Args &&...value) {
        //...
    }
};
int main(int argc, char *argv[]) {
    generic_lambda_helper helper;
    auto generic_lambda {bind(helper, placeholders::_1, placeholders::_2, placeholders::_3, placeholders::_4, placeholders::_5, placeholders::_6, placeholders::_7, placeholders::_8, placeholders::_9, placeholders::_10)};
}