摘要訊息 : 一些和 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
. 傳遞給 func
的 i
並不是 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::_1
到 std::placeholders::_10
. libstdc++ 中提供了 29 個佔位, 從 std::placeholders::_1
到 std::placeholders::_29
.
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :