摘要訊息 : 一些和 Lambda 表達式相關的注意事項.

0. 前言

《C++ 學習筆記》中, 我們已經詳細講述過了 Lambda 表達式, 這篇文章是針對《C++ 學習筆記》中的 Lambda 表達式進行補充. 如果閣下沒有學習過 C++ 中的 Lambda 表達式, 那麼建議先閱讀《C++ 學習筆記》.

更新紀錄 :

  • 2022 年 5 月 24 日進行第一次更新和修正.

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;
        }};
    }
};

如果要捕獲成員變數, 必須採用 [=] 或者 [&] 的形式. 不過需要注意的是, lambda 中可以使用 value 並不是被值捕獲或者參考捕獲的, 而是 value 是靜態成員變數, 它本身在 lambda 中就是可視的, 即使不補貨.

2. 優先選用 Lambda 表達式, 而非 std::bind

Lambda 表達式可以做到的, 一般來說 std::bind 也可以做到. 但是我們通常建議大家優選選用 Lambda 表達式, 除了易用性之外, 還有一些其它理由. 當然, std::bind 也並非一無是處.

2.1 優先選用 Lambda 表達式的第一個理由

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

class time;
class countdown;

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

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

#include <functional>

class time;
class countdown;

template <typename F>
void caller(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() 並不會回傳 one_minute_bind 被呼叫的時間, 而實際上回傳的是 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;

template <typename F>
void caller(time start, countdown seconds, F f);
void caller(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 (*)(time, countdown, void (*)(int))>(caller), std::bind(time::now), 60, std::placeholders::_1);

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

2.2 優先選用 Lambda 表達式的第二個理由

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

#include <functional>
#include <iostream>

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

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

答案是 0. 傳遞給 funci 並不是 level, 而是 std::bind 內部所產生的一個臨時值. 大家可能不了解 std::bind 內部的實作機制, 但是現在你只要知道 std::bind 在繫結引數的時候, 並非按照參考繫結. 那麼正確的方法是借助來自標頭檔 <functional> 中的 std::ref 或者 std::cref 來傳遞參考或者常數參考. Code 4 中的 callable_obj 的宣告應該改成 : auto callable_obj = std::bind(func, std::ref(level));. 這是盡量使用 Lambda 表達式的又一個理由.

2.3 std::bind 的用武之處

準確地說, 在 C++ 14 之後, std::bind 可以做的 Lambda 表達式都可以做. 但是在 C++ 11 中, 有兩個地方是 Lambda 表達式無法直接完成但是 std::bind 可以直接完成的. C++ 11 並沒有為 Lambda 表達式引入移動捕獲, 移動捕獲只有在 C++ 14 的泛型捕獲列表中才可以做到. 但是在 C++ 11 中, 我們可以配合 std::bind 和 Lambda 表達式來做到移動捕獲 :

#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) {
        //...
    }, std::move(vec))};
}

泛型 Lambda 表達式也是 C++ 14 才引入的, 所以要在 C++ 11 中模擬泛型 Lambda 表達式的話, 也需要借助 std::bind :

#include <iostream>

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

不過 std::bind 的佔位是有限提供的, 像 libc++ 中只提供了 10 個佔位, 即 std::placeholders::_1std::placeholders::_10. libstdc++ 中提供了 29 個佔位, 從 std::placeholders::_1std::placeholders::_29.