Lambda 表達式
本節解決什麼問題
很多地方需要傳入一小段邏輯:排序規則、過濾條件、按鈕回調、定時器回調。舊寫法通常有三種:
- 寫一個普通函數。
- 寫一個函數指針。
- 寫一個函數對象,也就是帶
operator()的類或結構體。
這些寫法都能工作,但小邏輯會被迫挪到遠處,或者要額外寫一個類型。Lambda 表達式讓你可以在使用現場直接寫一個小函數,而且可以捕獲外部變量。
這個特性是什麼
Lambda 是匿名可調用對象。它看起來像一個函數,但本質上是編譯器生成的一個類對象。
基本結構是:
| 部分 | 例子 | 含義 |
|---|---|---|
| 捕獲列表 | [x, &y] | 從外部作用域拿哪些變量 |
| 參數列表 | (int a, int b) | 調用 lambda 時傳入什麼 |
| 返回值 | -> int | 返回類型,通常可以省略 |
| 函數體 | { return a + b; } | 真正執行的代碼 |
完整形式可以寫成 [x](int n) -> int { return x + n; },常見情況下返回值類型可以省略。
C++ 標準版本
- C++11:基礎 Lambda。
- C++14:泛型 Lambda,也就是參數可以寫
auto。 - C++17:
constexprLambda、[*this]捕獲。
Lambda 是語言特性,不需要額外頭文件。只有配合 STL 算法、std::function 等庫工具時,才需要包含對應頭文件。
捕獲列表速查
| 寫法 | 含義 | 適用場景 |
|---|---|---|
[] | 不捕獲外部變量 | 只用參數或局部臨時變量 |
[x] | 按值捕獲 x | 保存一份副本,適合保存回調 |
[&x] | 按引用捕獲 x | 需要修改外部變量,且能保證生命週期 |
[x, &y] | 混合捕獲 | 明確表達哪些拷貝、哪些引用 |
[=] | 默認按值捕獲用到的變量 | 小例子方便,工程中不建議濫用 |
[&] | 默認按引用捕獲用到的變量 | 異步或保存回調中尤其危險 |
[this] | 捕獲當前對象指針 | 對象必須比 lambda 活得久 |
[*this] | 捕獲當前對象副本 | C++17 起可用,適合避免懸空 this |
示例代碼
示例 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
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 的優勢在於:如果這個判斷規則只在這裏使用一次,就可以直接寫在算法旁邊,不需要額外定義一個函數或結構體。
示例 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
示例 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
示例 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
爲什麼連續調用 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: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
示例中使用的 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()
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 使用的是同一種設計思想,只是它們向規則函數提出的問題不同。
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 | 已經找到,立即停止並返回當前位置 |
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 負責描述具體規則。
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
範圍 for
for (const auto& name : names)
{
std::cout << name << " ";
}
可以直接理解爲:
依次取出
names中的每個元素,並把當前元素命名爲name。
這裏的:
const auto& name
編譯器會根據 names 的元素類型自動推導,近似等價於:
const std::string& name
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 常用於表示容器大小、字符串長度和數組下標等非負數量。
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";
}
意思就是:
如果確實找到了元素,再使用它。
整段代碼的執行過程
程序首先創建:
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 負責描述具體規則。
示例 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 已經銷燬。
示例 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] 時要特別關注對象生命週期。
示例 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 內部保存的對象副本,不會修改外面的原對象。
關鍵語法解釋
| 示例 | 重點 | 說明 |
|---|---|---|
| 示例 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 起捕獲當前對象副本,與原對象狀態分離 |
常見錯誤
- 保存回調時使用默認引用捕獲
[&],導致局部變量銷燬後仍被訪問。 - 以爲按值捕獲會跟着外部變量變化。按值捕獲保存的是定義 lambda 時的副本。
- 想修改按值捕獲的副本,卻忘記加
mutable。 - 把有捕獲的 lambda 當成函數指針使用。有捕獲的 lambda 需要用模板參數、
auto變量或std::function保存。 - 異步場景捕獲
this後,對象先被銷燬。需要保證對象生命週期,或使用智能指針、[*this]等更明確的方式。
使用建議
- 小而局部的邏輯優先用 lambda。
- 捕獲列表儘量顯式寫
[x, &y],少用默認[=]和[&]。 - 保存回調、線程、定時器、異步操作中優先按值捕獲需要的數據。
- 只調用一次、不需要保存的回調,可以直接把 lambda 傳給算法或函數模板。
- 類成員函數中的回調使用
[this]時要保證對象生命週期;需要獨立保存對象狀態時可以考慮 C++17 的[*this]。 - 需要統一保存不同 lambda 時,再使用下一節的
std::function。
小結
- Lambda 是匿名可調用對象,寫法是
[捕获](参数) { 函数体 }。 - 捕獲列表決定 lambda 如何使用外部變量。
[x]是按值捕獲,[&x]是按引用捕獲。mutable允許修改按值捕獲的內部副本。[this]捕獲當前對象指針,[*this]從 C++17 起可以捕獲當前對象副本。- Lambda 最常用於 STL 算法、回調和異步任務。
- 生命週期是 Lambda 最容易出錯的地方,尤其是保存回調和異步回調。