Wiki
Lambda 表示式
本文由簡體中文內容確定性轉換,並受版本化術語表保護。
1.本節解決什麼問題
很多地方需要傳入一小段邏輯:排序規則、過濾條件、按鈕回撥、定時器回撥。舊寫法通常有三種:
- 寫一個普通函式。
- 寫一個函式指標。
- 寫一個函式物件,也就是帶
operator()的類或結構體。
這些寫法都能工作,但小邏輯會被迫挪到遠處,或者要額外寫一個型別。Lambda 表示式讓你可以在使用現場直接寫一個小函式,而且可以捕獲外部變數。
2.這個特性是什麼
Lambda 是匿名可呼叫物件。它看起來像一個函式,但本質上是編譯器生成的一個類物件。
基本結構是:
| 部分 | 例子 | 含義 |
|---|---|---|
| 捕獲列表 | [x, &y] |
從外部作用域拿哪些變數 |
| 引數列表 | (int a, int b) |
呼叫 lambda 時傳入什麼 |
| 返回值 | -> int |
返回型別,通常可以省略 |
| 函式體 | { return a + b; } |
真正執行的程式碼 |
完整形式可以寫成 [x](int n) -> int { return x + n; },常見情況下返回值型別可以省略。
3.C++ 標準版本
- C++11:基礎 Lambda。
- C++14:泛型 Lambda,也就是引數可以寫
auto。 - C++17:
constexprLambda、[*this]捕獲。
Lambda 是語言特性,不需要額外標頭檔案。只有配合 STL 演算法、std::function 等庫工具時,才需要包含對應標頭檔案。
4.捕獲列表速查
| 寫法 | 含義 | 適用場景 |
|---|---|---|
[] |
不捕獲外部變數 | 只用引數或區域性臨時變數 |
[x] |
按值捕獲 x |
儲存一份副本,適合儲存回撥 |
[&x] |
按引用捕獲 x |
需要修改外部變數,且能保證生命週期 |
[x, &y] |
混合捕獲 | 明確表達哪些複製、哪些引用 |
[=] |
預設按值捕獲用到的變數 | 小例子方便,工程中不建議濫用 |
[&] |
預設按引用捕獲用到的變數 | 非同步或儲存回撥中尤其危險 |
[this] |
捕獲當前物件指標 | 物件必須比 lambda 活得久 |
[*this] |
捕獲當前物件副本 | C++17 起可用,適合避免懸空 this |
5.示例程式碼
5.1.示例 1:舊回撥寫法和 lambda 的區別
普通函式和函式物件都可以作為演算法條件,但 Lambda 更適合寫簡短的區域性邏輯。
#include <algorithm>
#include <iostream>
#include <vector>
bool is_even(int n)
{
return n % 2 == 0;
}
struct GreaterThan
{
int limit;
bool operator()(int n) const
{
return n > limit;
}
};
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
// vector 是动态数组,元素数量可以在运行时变化。
std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
int even_count = std::count_if(numbers.begin(), numbers.end(), is_even);
std::cout << "even count = " << even_count << "\n";
int greater_count1 = std::count_if(numbers.begin(), numbers.end(), GreaterThan{3});
std::cout << "> 3 count (functor) = " << greater_count1 << "\n";
int limit = 3;
int greater_count2 = std::count_if(numbers.begin(), numbers.end(),
[limit](int n) {
return n > limit;
});
std::cout << "> 3 count (lambda) = " << greater_count2 << "\n";
return 0;
}
執行結果:
even count = 3
> 3 count (functor) = 3
> 3 count (lambda) = 3
5.1.1.count_if 的第三個引數是什麼?
先看這一句:
int even_count = std::count_if(numbers.begin(), numbers.end(), is_even);
std::count_if 可以簡單理解為:
從指定範圍中逐個取出元素,呼叫你提供的“判斷規則”,統計其中返回
true的元素有多少個。
它的基本形式是:
std::count_if(起点, 终点, 判断规则);
第三個引數就是這個“判斷規則”。它並不一定非得是普通函式,只要是可以像函式一樣呼叫的物件就可以,包括普通函式、函式物件和 Lambda。這些東西統稱為可呼叫物件(Callable)。
這裡第三個引數傳入的是:
is_even
而 is_even 是:
bool is_even(int n)
{
return n % 2 == 0;
}
count_if 會把 numbers 中的元素一個一個傳給它,過程可以近似理解成:
is_even(1); // false,不计数
is_even(2); // true,计数 +1
is_even(3); // false,不计数
is_even(4); // true,计数 +1
is_even(5); // false,不计数
is_even(6); // true,计数 +1
因此最後得到 3。
可以把 count_if 的內部工作近似理解成:
int count = 0;
for (每一个元素)
{
if (判断规则(当前元素))
{
++count;
}
}
也就是說,count_if 自己負責:
- 怎麼遍歷容器;
- 怎麼統計數量。
第三個引數負責告訴它:
什麼樣的元素才算符合條件?
同一個位置也可以傳入函式物件:
std::count_if(numbers.begin(), numbers.end(), GreaterThan{3});
GreaterThan{3} 可以像函式一樣被呼叫,因為結構體定義了:
bool operator()(int n) const
{
return n > limit;
}
它回答的問題就是:
當前這個
n是否大於3?
還可以直接傳 Lambda:
std::count_if(numbers.begin(), numbers.end(),
[limit](int n) {
return n > limit;
});
count_if 同樣會反覆呼叫這個 Lambda:
lambda(1); // false
lambda(2); // false
lambda(3); // false
lambda(4); // true
lambda(5); // true
lambda(6); // true
所以這三種寫法雖然形式不同,本質上都是在給 count_if 提供一個判斷規則:
is_even
GreaterThan{3}
[limit](int n) { return n > limit; }
Lambda 的優勢在於:如果這個判斷規則只在這裡使用一次,就可以直接寫在演算法旁邊,不需要額外定義一個函式或結構體。
5.2.示例 2:最基本的 lambda 語法、引數和返回值
#include <iostream>
#include <string>
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
auto add = [](int a, int b) {
return a + b;
};
auto describe_score = [](int score) -> std::string {
if (score >= 60)
{
return "pass";
}
return "fail";
};
std::cout << "add(3, 5) = " << add(3, 5) << "\n";
std::cout << "score 80 is " << describe_score(80) << "\n";
std::cout << "score 40 is " << describe_score(40) << "\n";
// 返回 0 表示程序正常结束。
return 0;
}
執行結果:
add(3, 5) = 8
score 80 is pass
score 40 is fail
5.3.示例 3:按值捕獲和按引用捕獲
按值捕獲會儲存定義 lambda 時的副本;按引用捕獲會訪問外部變數本身。
#include <iostream>
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
int score = 10;
auto add_by_value = [score](int bonus) {
return score + bonus;
};
auto add_by_ref = [&score](int bonus) {
score += bonus;
return score;
};
score = 20;
std::cout << "value capture result = " << add_by_value(5) << "\n";
std::cout << "ref capture result = " << add_by_ref(5) << "\n";
std::cout << "score after ref capture = " << score << "\n";
// 返回 0 表示程序正常结束。
return 0;
}
執行結果:
value capture result = 15
ref capture result = 25
score after ref capture = 25
5.4.示例 4:mutable 允許修改按值捕獲的副本
按值捕獲的變數預設在 lambda 內是隻讀的。加上 mutable 後,可以修改 lambda 自己儲存的副本,但不會修改外部變數。
#include <iostream>
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
int start = 0;
auto counter = [start]() mutable {
++start;
return start;
};
std::cout << "counter() = " << counter() << "\n";
std::cout << "counter() = " << counter() << "\n";
std::cout << "outside start = " << start << "\n";
// 返回 0 表示程序正常结束。
return 0;
}
執行結果:
counter() = 1
counter() = 2
outside start = 0
5.4.1.為什麼連續呼叫 counter() 會繼續累加?
這裡不是因為外部 start 的作用域變大了,而是因為 Lambda 物件 counter 自己儲存了一份 start。
int start = 0;
auto counter = [start]() mutable {
++start;
return start;
};
[start] 是按值捕獲。可以近似理解為編譯器生成了這樣的物件:
struct Counter
{
int start;
int operator()()
{
++start;
return start;
}
};
Counter counter{start};
因此程式中實際上有兩個不同的 start:
- 外部
start:始終還是0。 counter內部儲存的start:第一次呼叫變成1,第二次呼叫繼續變成2。
counter(); // 内部 start:0 -> 1
counter(); // 内部 start:1 -> 2
關鍵在於:
auto counter = [start]() mutable { ... };
這裡只建立了一次 Lambda 物件。後面的兩個 counter() 都是在呼叫同一個物件,不會每次呼叫時重新捕獲一次外部 start。
mutable 的作用則是允許修改 Lambda 內部按值捕獲的副本,它並不會讓外部的 start 一起變化。
5.5.示例 5:lambda 配合 STL 演算法
Lambda 和 STL 演算法配合時最常見:排序、查詢、計數、轉換都可以把區域性邏輯直接寫在呼叫處。
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
// vector 是动态数组,元素数量可以在运行时变化。
std::vector<std::string> names = {"Bob", "Alice", "Charlie", "David"};
std::sort(names.begin(), names.end(),
[](const std::string& a, const std::string& b) {
if (a.size() == b.size())
{
return a < b;
}
return a.size() < b.size();
});
std::cout << "sort by length: ";
for (const auto& name : names)
{
std::cout << name << " ";
}
std::cout << "\n";
int min_length = 6;
auto it = std::find_if(names.begin(), names.end(),
[min_length](const std::string& name) {
return name.size() >= static_cast<std::size_t>(min_length);
});
if (it != names.end())
{
std::cout << "first long name = " << *it << "\n";
}
return 0;
}
執行結果:
sort by length: Bob Alice David Charlie
first long name = Charlie
5.5.1.示例中使用的 API 和語法
這個示例除了 Lambda,還使用了一些常見的 STL API。
| 寫法 | 簡單說明 |
|---|---|
std::vector<std::string> |
動態陣列,這裡用於儲存多個字串 |
names.begin() |
返回指向第一個元素的迭代器 |
names.end() |
返回最後一個元素後一個位置的迭代器 |
std::sort(begin, end, rule) |
對指定範圍排序,第三個引數指定比較規則 |
string.size() |
返回字串長度 |
const std::string& |
只讀引用字串,避免額外複製 |
for (const auto& x : container) |
範圍 for,依次訪問容器中的每個元素 |
auto |
讓編譯器自動推導變數型別 |
std::find_if(begin, end, rule) |
查詢第一個滿足條件的元素 |
static_cast<T>(value) |
將 value 顯式轉換為型別 T |
std::size_t |
STL 中常用於表示大小、數量和下標的無符號整數型別 |
*it |
取得迭代器 it 當前指向的元素 |
其中 begin() 和 end() 經常一起出現:
names.begin(), names.end()
表示整個 names 的遍歷範圍。需要注意,end() 並不指向最後一個元素,而是最後一個元素後面的結束位置:
Bob Alice David Charlie [结束位置]
↑ ↑
begin() end()
5.5.2.STL 演算法為什麼要接收一個“規則函式”?
std::count_if、std::find_if、std::sort 有一個共同特點:
演算法自己負責“怎麼做”,你提供的函式負責“按照什麼規則做”。
例如 std::sort 自己知道排序演算法應該怎樣移動和比較元素,但是它不知道你希望按照數值大小、字串長度還是其他規則排序。
同樣,std::find_if 自己知道怎樣從前往後查詢,但是它不知道“什麼樣的元素才算你要找的元素”。
因此這些演算法會接收一個可呼叫物件,並在執行過程中反覆呼叫它。
在示例 1 中已經看到:
std::count_if(begin, end, rule);
它會對每個元素呼叫:
rule(element);
第三個引數回答的是:
這個元素算不算?
find_if 和 sort 使用的是同一種設計思想,只是它們向規則函式提出的問題不同。
5.5.3.std::find_if:這個元素是不是我要找的?
基本形式是:
std::find_if(起点, 终点, 判断规则);
示例中:
auto it = std::find_if(names.begin(), names.end(),
[min_length](const std::string& name) {
return name.size() >= static_cast<std::size_t>(min_length);
});
[min_length] 按值捕獲最小長度 6。這個 Lambda 接收一個字串,並判斷它的長度是否至少為 6。
find_if 會依次呼叫這個 Lambda,可以近似理解成:
rule("Bob"); // false
rule("Alice"); // false
rule("David"); // false
rule("Charlie"); // true
一旦第一次得到 true,find_if 就停止查詢,並返回當前元素的位置。
因此 find_if 的第三個引數回答的是:
當前元素是不是我要找的?
它和 count_if 的規則函式形式很像,通常都是:
bool rule(一个元素);
區別在於演算法得到 true 之後做什麼:
| 演算法 | 規則返回 true 後的行為 |
|---|---|
count_if |
計數加一,然後繼續檢查後面的元素 |
find_if |
已經找到,立即停止並返回當前位置 |
5.5.4.std::sort:a 應不應該排在 b 前面?
std::sort 稍微不同:
std::sort(起点, 终点, 比较规则);
它的第三個引數一次接收兩個元素:
[](const std::string& a, const std::string& b) {
if (a.size() == b.size())
{
return a < b;
}
return a.size() < b.size();
}
sort 會在排序過程中不斷呼叫:
rule(a, b);
這個函式要回答:
按照當前排序規則,
a是否應該排在b前面?
例如:
rule("Bob", "Charlie");
因為:
Bob 长度 3
Charlie 长度 7
最終判斷的是:
3 < 7
結果為 true,表示按照當前規則,Bob 應該排在 Charlie 前面。
如果兩個字串長度相同:
if (a.size() == b.size())
{
return a < b;
}
就再使用字串的字典順序決定誰在前面。例如 Alice 和 David 長度都是 5,因此比較:
"Alice" < "David"
結果為 true,所以 Alice 排在 David 前面。
三個演算法可以這樣記:
| API | 第三個引數回答什麼? |
|---|---|
count_if |
這個元素算不算? |
find_if |
這個元素是不是我要找的? |
sort |
a 應不應該排在 b 前面? |
這正是 Lambda 很常見的用途:STL 演算法負責通用流程,Lambda 負責描述具體規則。
5.5.5.const std::string& 和 .size()
Lambda 中的:
const std::string& a
可以拆成:
std::string:引數是字串;&:使用引用,不額外複製整個字串;const:只讀取這個字串,不修改它。
因此 const std::string& 常用於只讀地接收 string、vector 等較大的物件。
字串的:
a.size()
返回字串長度。例如:
"Bob" -> 3
"Alice" -> 5
"Charlie" -> 7
"David" -> 5
所以示例的排序結果是:
Bob Alice David Charlie
5.5.6.範圍 for
for (const auto& name : names)
{
std::cout << name << " ";
}
可以直接理解為:
依次取出
names中的每個元素,並把當前元素命名為name。
這裡的:
const auto& name
編譯器會根據 names 的元素型別自動推導,近似等價於:
const std::string& name
5.5.7.static_cast<std::size_t>
這裡:
static_cast<std::size_t>(min_length)
是顯式型別轉換,可以簡單理解為:
把
min_length轉換成std::size_t型別。
因為 name.size() 返回的是 std::size_t,而:
int min_length = 6;
中的 min_length 是 int。轉換以後,兩邊使用適合的同類整數型別進行比較。
std::size_t 常用於表示容器大小、字串長度和陣列下標等非負數量。
5.5.8.it 和迭代器
auto it = std::find_if(...);
find_if 返回的不是字串本身,而是一個迭代器。
迭代器可以暫時理解成類似指標的物件,用來表示容器中的某個位置。
排序後的資料是:
Bob Alice David Charlie
↑
it
因此:
*it
取得的就是:
Charlie
如果 find_if 一直沒有找到滿足條件的元素,它會返回:
names.end()
所以常見寫法是:
if (it != names.end())
{
std::cout << *it << "\n";
}
意思就是:
如果確實找到了元素,再使用它。
5.5.9.整段程式碼的執行過程
程式首先建立:
std::vector<std::string> names = {
"Bob",
"Alice",
"Charlie",
"David"
};
然後 std::sort 使用 Lambda 作為排序規則:
- 長度不同,短的排前面。
- 長度相同,按照字串字典順序排列。
所以得到:
Bob Alice David Charlie
接下來:
int min_length = 6;
規定要尋找長度至少為 6 的名字。
std::find_if 會按照當前順序依次呼叫 Lambda:
Bob -> 3 >= 6 -> false
Alice -> 5 >= 6 -> false
David -> 5 >= 6 -> false
Charlie -> 7 >= 6 -> true
第一次得到 true 時停止,因此 it 最終指向 Charlie:
*it
得到:
Charlie
最終輸出:
sort by length: Bob Alice David Charlie
first long name = Charlie
這個示例體現了 Lambda 非常典型的一種用途:
STL 演算法負責通用流程,Lambda 負責描述具體規則。
5.6.示例 6:儲存回撥時要注意捕獲生命週期
如果 lambda 只是立刻呼叫,引用捕獲通常看起來沒問題;如果儲存到容器、執行緒、定時器或非同步回撥裡,lambda 可能晚於區域性變數執行。儲存回撥時優先按值捕獲需要的資料。
#include <functional>
#include <iostream>
#include <string>
#include <vector>
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
// std::function 可以保存普通函数、lambda 或函数对象。
// vector 是动态数组,元素数量可以在运行时变化。
std::vector<std::function<void()>> callbacks;
{
std::string name = "Alice";
int score = 95;
auto print_now = [&name, &score]() {
std::cout << "now: " << name << " " << score << "\n";
};
print_now();
callbacks.push_back([name, score]() {
std::cout << "saved: " << name << " " << score << "\n";
});
}
for (const auto& callback : callbacks)
{
callback();
}
return 0;
}
執行結果:
now: Alice 95
saved: Alice 95
這裡 print_now 立刻呼叫,所以引用捕獲沒問題。儲存到 callbacks 的 lambda 用按值捕獲,因為離開內部作用域後,name 和 score 已經銷燬。
5.7.示例 7:捕獲 this 訪問當前物件
在類的成員函式中,Lambda 可以使用 [this] 捕獲當前物件的指標。Lambda 訪問的仍然是原來的物件,因此物件成員之後發生的變化也能看到。
#include <iostream>
class Robot
{
public:
Robot(int speed) : speed_(speed)
{
}
void demo()
{
auto print_speed = [this]() {
std::cout << "lambda speed = " << speed_ << "\n";
};
speed_ = 20;
print_speed();
}
private:
int speed_;
};
int main()
{
Robot robot(10);
robot.demo();
return 0;
}
執行結果:
lambda speed = 20
這裡:
[this]() {
std::cout << speed_;
}
可以近似理解為 Lambda 儲存了當前物件的 this 指標,因此:
speed_
實際上訪問的是:
this->speed_
建立 Lambda 時 speed_ 是 10,之後原物件把 speed_ 修改成 20。因為 [this] 指向的仍然是原物件,所以呼叫 Lambda 時讀到的是 20。
需要特別注意:[this] 儲存的是物件指標,不是物件副本。如果 Lambda 被儲存下來,而原物件已經銷燬,再通過這個 Lambda 訪問成員就可能產生懸空指標問題。因此儲存回撥、非同步任務和執行緒回撥中使用 [this] 時要特別關注物件生命週期。
5.8.示例 8:使用 [*this] 捕獲當前物件副本
C++17 開始可以使用 [*this]。它不是儲存當前物件的指標,而是在建立 Lambda 時複製一份當前物件。
#include <iostream>
class Robot
{
public:
Robot(int speed) : speed_(speed)
{
}
void demo()
{
auto print_speed = [*this]() {
std::cout << "lambda copy speed = " << speed_ << "\n";
};
speed_ = 20;
print_speed();
std::cout << "outside speed = " << speed_ << "\n";
}
private:
int speed_;
};
int main()
{
Robot robot(10);
robot.demo();
return 0;
}
執行結果:
lambda copy speed = 10
outside speed = 20
這裡 Lambda 建立時:
[*this]
會把當時的 Robot 物件複製一份儲存到 Lambda 內部。此時副本中的:
speed_ = 10
之後:
speed_ = 20;
修改的是外面的原物件,不會修改 Lambda 已經儲存的物件副本,因此 Lambda 仍然輸出 10。
可以把兩種寫法這樣對比:
| 寫法 | Lambda 儲存什麼 | 原物件後來修改成員後 |
|---|---|---|
[this] |
當前物件的指標 | Lambda 能看到新的成員值 |
[*this] |
當前物件的副本 | Lambda 保留捕獲時的成員值 |
[*this] 可以避免因為原物件銷燬而直接留下一個懸空的 this 指標,但它會複製整個物件,因此也要考慮物件是否適合複製以及複製成本。
如果需要在 Lambda 中修改這份物件副本,與普通按值捕獲類似,可以再配合 mutable:
auto callback = [*this]() mutable {
++speed_;
};
這裡修改的仍然只是 Lambda 內部儲存的物件副本,不會修改外面的原物件。
6.關鍵語法解釋
| 示例 | 重點 | 說明 |
|---|---|---|
| 示例 1 | 舊回撥寫法對比 | 普通函式、函式物件都能做回撥,但 lambda 更適合區域性短邏輯 |
| 示例 2 | 引數和返回值 | 返回型別通常可推導,分支返回不同型別時要顯式寫清楚 |
| 示例 3 | 捕獲列表 | [x] 複製,[&x] 引用 |
| 示例 4 | mutable |
修改的是 lambda 內部副本,不影響外部變數 |
| 示例 5 | STL 演算法 | sort、find_if、count_if 常和 lambda 配合 |
| 示例 6 | 生命週期 | 儲存回撥、非同步回撥、執行緒回撥中不要隨便引用捕獲區域性變數 |
| 示例 7 | [this] |
捕獲當前物件指標,Lambda 訪問的是原物件 |
| 示例 8 | [*this] |
C++17 起捕獲當前物件副本,與原物件狀態分離 |
7.常見錯誤
- 儲存回撥時使用預設引用捕獲
[&],導致區域性變數銷燬後仍被訪問。 - 以為按值捕獲會跟著外部變數變化。按值捕獲儲存的是定義 lambda 時的副本。
- 想修改按值捕獲的副本,卻忘記加
mutable。 - 把有捕獲的 lambda 當成函式指標使用。有捕獲的 lambda 需要用模板引數、
auto變數或std::function儲存。 - 非同步場景捕獲
this後,物件先被銷燬。需要保證物件生命週期,或使用智慧指標、[*this]等更明確的方式。
8.使用建議
- 小而區域性的邏輯優先用 lambda。
- 捕獲列表儘量顯式寫
[x, &y],少用預設[=]和[&]。 - 儲存回撥、執行緒、定時器、非同步操作中優先按值捕獲需要的資料。
- 只調用一次、不需要儲存的回撥,可以直接把 lambda 傳給演算法或函式模板。
- 類成員函式中的回撥使用
[this]時要保證物件生命週期;需要獨立儲存物件狀態時可以考慮 C++17 的[*this]。 - 需要統一儲存不同 lambda 時,再使用下一節的
std::function。
9.小結
- Lambda 是匿名可呼叫物件,寫法是
[捕获](参数) { 函数体 }。 - 捕獲列表決定 lambda 如何使用外部變數。
[x]是按值捕獲,[&x]是按引用捕獲。mutable允許修改按值捕獲的內部副本。[this]捕獲當前物件指標,[*this]從 C++17 起可以捕獲當前物件副本。- Lambda 最常用於 STL 演算法、回撥和非同步任務。
- 生命週期是 Lambda 最容易出錯的地方,尤其是儲存回撥和非同步回撥。