在文章《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
. 傳遞給 func
的 i
並不是 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)};
}
自創文章, 原著 : Jonny, 如若需要轉發, 在已經授權的情況下請註明出處 :《【C++】Lambda 表達式補充》https://jonny.vip/2020/10/08/%e3%80%90cplusplus%e3%80%91lambda-%e8%a1%a8%e9%81%94%e5%bc%8f%e8%a3%9c%e5%85%85/
Leave a Reply