Lambda 是「寫在表達式裡的匿名函式」。它讓你把一小段邏輯就地定義、就地使用,不必為了三行程式碼特地跑到別處寫一個具名函式。
在 Ch04 我們學會了函式,在 Ch06 認識了函式指標。Lambda 是這條路的自然延伸:它比函式指標更強大(可以「帶著狀態」),比具名函式更輕便(不必命名、不必宣告),是現代 C++ 中使用頻率極高的語言特性。
學習目標
- 理解 lambda 解決什麼問題:為什麼有了函式還需要 lambda
- 掌握 lambda 的完整語法:捕獲列表、參數、回傳型別、函式主體
- 深入理解捕獲(capture):傳值捕獲、傳參考捕獲、預設捕獲與其陷阱
- 理解
mutable的意義與傳值捕獲的「副本」語意 - 學會用
auto儲存 lambda,並認識泛型 lambda(auto參數) - 理解無捕獲 lambda 與函式指標的關係(銜接 Ch06)
- 能將 lambda 與陣列/
std::vector的走訪、排序、搜尋結合使用 - 認識懸垂捕獲(dangling capture)等常見陷阱
一、為什麼需要 Lambda?
1.1 從一個排序需求開始
假設要把成績由大到小排序。用 std::sort(來自 <algorithm>)預設是由小到大,想改順序就得提供「比較規則」。在沒有 lambda 的年代,你必須先寫一個具名函式:
#include <algorithm>
#include <vector>
// 為了三行邏輯,特地寫在外面的函式
bool descending(int a, int b) {
return a > b;
}
int main() {
std::vector<int> scores = {70, 95, 88, 60};
std::sort(scores.begin(), scores.end(), descending); // 傳函式當參數
}
這有兩個問題:
| 問題 | 說明 |
|---|---|
| 邏輯被拆散 | 比較規則寫在離使用處很遠的地方,閱讀時要跳來跳去 |
| 命名負擔 | 只用一次的小函式也得取名字、佔據全域空間 |
1.2 用 lambda 就地解決
std::sort(scores.begin(), scores.end(),
[](int a, int b) { return a > b; }); // 規則就寫在使用處
[](int a, int b) { return a > b; } 就是一個 lambda 表達式:一個沒有名字、直接寫在需要它的地方的函式。
1.3 lambda 比函式指標多了什麼?
Ch06 學過的函式指標也能傳遞行為,但它有個根本限制:函式看不到呼叫處的區域變數。
int passing = 60; // 及格門檻是執行期才決定的區域變數
// 具名函式無法「帶著」passing —— 除非用全域變數(壞主意)
bool isPass(int score) { return score >= /* 拿不到 passing! */ 60; }
Lambda 用捕獲(capture)解決這個問題:
int passing = 60;
auto isPass = [passing](int score) { return score >= passing; }; // 把 passing「帶進去」
isPass(75); // true
這種「函式 + 攜帶的狀態」的組合,正式名稱叫閉包(closure)。這是 lambda 相對於一般函式最大的威力來源。
二、Lambda 基本語法
2.1 語法解剖
[捕獲列表](參數列表) -> 回傳型別 { 函式主體 }
↑ ↑ ↑ ↑
capture parameters return type body
(必要) (可省略) (可省略) (必要)
// 最小的 lambda:不捕獲、無參數、無回傳
auto hello = []() { std::cout << "Hello, Lambda!\n"; };
hello(); // 像呼叫函式一樣用 () 呼叫
// 有參數、自動推導回傳型別
auto add = [](int a, int b) { return a + b; };
std::cout << add(3, 4); // 7
// 明確標註回傳型別(-> 語法)
auto divide = [](double a, double b) -> double {
if (b == 0) return 0;
return a / b;
};
各部位規則:
[]捕獲列表:即使不捕獲任何東西也不能省略——[]正是編譯器辨認「這是 lambda」的記號。()參數列表:寫法與一般函式參數完全相同(也可用const T&等),無參數時可整個省略([]{ ... })。-> T回傳型別:通常省略,由return語句自動推導。當主體有多條return且型別不一致時,才需要明確標註。{}主體:與一般函式主體相同。
2.2 用 auto 儲存 lambda
每個 lambda 都有一個編譯器生成的、獨一無二的型別,你寫不出它的名字,所以要存進變數時必用 auto:
auto square = [](int x) { return x * x; };
std::cout << square(5); // 25
也可以定義完立刻呼叫(IIFE,Immediately Invoked Function Expression),常用來初始化複雜的 const 變數:
const int config = [](){
int value = 10;
// ...一連串複雜的計算...
return value * 3;
}(); // 注意結尾的 () —— 立刻呼叫
2.3 lambda 到底是什麼?
Lambda 不是魔法:編譯器會把它轉換成一個「可以像函式一樣被呼叫的物件」,捕獲的變數就是這個物件內部的資料。現在只需要建立這個直覺——「lambda 是一個帶著資料、可被呼叫的物件」——它的完整原理(函式物件 / functor)會在學完類別後於 Ch15 深入剖析。
三、捕獲列表(Capture List)——本章核心
捕獲決定「lambda 可以使用哪些外部的區域變數、以及用什麼方式使用」。
3.1 傳值捕獲 [x]
lambda 在建立的那一刻把變數複製一份進來。之後外面的變數怎麼變,都與 lambda 內的副本無關。
int n = 10;
auto f = [n]() { std::cout << n << '\n'; }; // 此刻複製 n(副本 = 10)
n = 999; // 修改外面的 n
f(); // 印出 10 —— lambda 用的是「當時」的副本
3.2 傳參考捕獲 [&x]
lambda 存的是外部變數的參考(別名):讀到的永遠是最新值,也可以修改外部變數。
int counter = 0;
auto increment = [&counter]() { counter++; }; // 捕獲參考
increment();
increment();
std::cout << counter; // 2 —— 外部變數真的被改了
3.3 混合捕獲與預設捕獲
int a = 1, b = 2, c = 3;
[a, &b] // a 傳值、b 傳參考(逐一指定,最清楚)
[=] // 預設「全部傳值」:用到的變數都自動複製
[&] // 預設「全部傳參考」
[=, &b] // 預設傳值,但 b 例外用參考
[&, a] // 預設傳參考,但 a 例外用傳值
最佳實踐:偏好明確列出要捕獲的變數(如
[a, &b])。[=]和[&]雖然方便,但會默默捕獲你沒意識到的變數,是懸垂參考等 bug 的溫床(見第七節陷阱)。
3.4 捕獲 vs 參數,怎麼分?
兩者都能把資料「送進」lambda,用途卻不同:
捕獲 [x] |
參數 (int x) |
|
|---|---|---|
| 資料何時決定 | lambda 建立時 | lambda 每次呼叫時 |
| 適合什麼 | 環境/設定值(如及格門檻) | 每次呼叫都不同的輸入(如被檢查的分數) |
int threshold = 60;
auto isPass = [threshold](int score) { return score >= threshold; };
// ↑ 建立時就固定的環境 ↑ 每次呼叫都不同的輸入
3.5 mutable:修改「傳值捕獲的副本」
傳值捕獲的副本預設是唯讀的,在 lambda 內修改會編譯錯誤。加上 mutable 才允許修改副本(外部原變數仍不受影響):
int n = 0;
auto counterA = [n]() mutable { n++; return n; }; // 修改的是 lambda 自己的副本
counterA(); // 1
counterA(); // 2 —— 副本的狀態會跨呼叫保留!
std::cout << n; // 0 —— 外部的 n 完全沒變
這揭示一件重要的事:lambda 物件有自己的狀態。副本活在 lambda 物件裡,每次呼叫共用同一份,所以能「記住」上次的值。
3.6 什麼東西不需要捕獲?
捕獲只針對外層函式的區域變數。以下不需要(也不能)捕獲:
- 全域變數、
static變數——直接使用即可 - 函式參數以外的常數表達式(如
constexpr常數) - 其他函式——直接呼叫即可
四、Lambda 與 STL 演算法
Lambda 最主要的舞台就是搭配 <algorithm> 的演算法函式,對容器(Ch05 的 std::vector)進行排序、搜尋、統計。
4.1 排序:std::sort
#include <algorithm>
#include <vector>
std::vector<int> v = {5, 2, 8, 1, 9};
std::sort(v.begin(), v.end()); // 預設遞增
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; }); // 遞減
// 依「絕對值」排序 —— 自訂規則的威力
std::vector<int> w = {-5, 2, -8, 1};
std::sort(w.begin(), w.end(),
[](int a, int b) { return std::abs(a) < std::abs(b); });
// w = {1, 2, -5, -8}
4.2 統計與搜尋:count_if、find_if、all_of
std::vector<int> scores = {75, 42, 90, 58, 66};
int passing = 60;
// 幾個人及格?(注意捕獲 passing)
int passed = std::count_if(scores.begin(), scores.end(),
[passing](int s) { return s >= passing; }); // 3
// 找第一個不及格的
auto it = std::find_if(scores.begin(), scores.end(),
[passing](int s) { return s < passing; });
if (it != scores.end()) std::cout << "第一個不及格:" << *it; // 42
// 是否全員及格?
bool allPass = std::all_of(scores.begin(), scores.end(),
[passing](int s) { return s >= passing; }); // false
4.3 逐一處理:for_each 與範圍 for
std::for_each(v.begin(), v.end(), [](int& x) { x *= 2; }); // 全部翻倍(注意 int&)
簡單走訪用範圍 for 就好(
for (int& x : v) x *= 2;)。for_each加 lambda 的價值在於「處理邏輯本身是一個變數」的場合——例如同一段流程要套用不同的處理方式。
4.4 轉換:std::transform
std::vector<int> nums = {1, 2, 3, 4};
std::vector<int> squares(nums.size());
std::transform(nums.begin(), nums.end(), squares.begin(),
[](int x) { return x * x; });
// squares = {1, 4, 9, 16}
五、泛型 Lambda(C++14)
參數型別寫 auto,同一個 lambda 就能吃各種型別——相當於一個小型模板(模板詳見 Ch12):
auto printTwice = [](const auto& x) { std::cout << x << ' ' << x << '\n'; };
printTwice(42); // 42 42
printTwice(3.14); // 3.14 3.14
printTwice("hello"); // hello hello
auto add = [](auto a, auto b) { return a + b; };
add(1, 2); // int:3
add(1.5, 2.5); // double:4.0
add(std::string("ab"), std::string("cd")); // string:"abcd"
六、無捕獲 Lambda 與函式指標(銜接 Ch06)
沒有捕獲任何東西的 lambda 可以隱式轉換成一般的函式指標,因此可以傳給任何接受函式指標的 API(包括 C 函式庫):
// Ch06 學過的函式指標
int (*fp)(int, int) = [](int a, int b) { return a + b; }; // ✓ 無捕獲,可轉換
std::cout << fp(3, 4); // 7
int base = 10;
int (*fp2)(int) = [base](int x) { return x + base; }; // ✗ 編譯錯誤!
為什麼有捕獲就不行? 函式指標只是「一個程式碼位址」,沒有地方存放捕獲的資料;而有捕獲的 lambda 是「程式碼 + 資料」的物件,塞不進一個純位址。這也再次印證第 2.3 節的觀點:lambda 本質上是物件,不是單純的函式。
最佳實踐(Best Practices)
- F.50:當函式「用完即丟」或需要捕獲區域狀態時用 lambda;邏輯會被重複使用、或超過幾行時,寫成具名函式。
- F.52 / F.53:在當場使用、不會存活超過目前作用域的 lambda 中,傳參考捕獲沒問題;但若 lambda 會被儲存下來稍後呼叫(回呼、非同步),務必用傳值捕獲,避免懸垂參考。
- 明確捕獲優於預設捕獲:寫
[a, &b]而不是[=]、[&],讓依賴一目瞭然。 - lambda 主體保持短小(1~5 行為宜);太長代表它應該是一個具名函式。
- 存 lambda 一律用
auto;參數若是大型物件,跟一般函式一樣用const T&。 - 善用 lambda + 演算法取代手寫迴圈:
count_if、find_if、all_of讓意圖更明確。
重點整理
| 概念 | 要點 |
|---|---|
| 語法 | [捕獲](參數) -> 回傳型別 { 主體 };[] 不可省略 |
| 閉包 | 函式 + 捕獲的狀態;lambda 本質是可呼叫的物件 |
傳值捕獲 [x] |
建立當下複製一份,之後與外部脫鉤 |
傳參考捕獲 [&x] |
別名,讀到最新值、可修改外部變數,小心懸垂 |
預設捕獲 [=] [&] |
方便但易藏 bug,偏好明確列出 |
mutable |
允許修改「傳值捕獲的副本」,外部不受影響;副本狀態跨呼叫保留 |
| 捕獲 vs 參數 | 捕獲 = 建立時的環境;參數 = 每次呼叫的輸入 |
| 泛型 lambda | 參數寫 auto,一個 lambda 適用多種型別(C++14) |
| 與函式指標 | 無捕獲的 lambda 可轉函式指標;有捕獲的不行 |
| 主要用途 | 搭配 sort、count_if、find_if、transform 等演算法 |
常見錯誤與陷阱
- 懸垂參考捕獲(最危險):lambda 以
[&]或[&x]捕獲區域變數,卻在該變數已銷毀後才被呼叫(例如把 lambda 回傳出去、存起來稍後執行)→ 未定義行為。會被存活下來的 lambda 請用傳值捕獲。
auto makeBad() {
int local = 42;
return [&local]() { return local; }; // ✗ 回傳後 local 已銷毀 → UB
}
- 以為傳值捕獲會「看到最新值」:傳值捕獲在 lambda 建立時就定格了,之後外部變數再怎麼改都無關。需要最新值請用參考捕獲。
- 忘記
mutable就修改傳值捕獲:[n]() { n++; }編譯錯誤;要嘛加mutable(改副本),要嘛用[&n](改本尊),先想清楚你要哪一種。 - 誤會
mutable會改到外部變數:mutable只解鎖「副本」的修改權,外部變數永遠不受影響。 [=]捕獲了大物件:預設傳值捕獲會複製所有用到的變數,若不小心用到大型vector就是一次昂貴的複製。明確捕獲、必要時用const&語意(傳參考捕獲且不修改)。- 有捕獲的 lambda 硬塞給函式指標:只有無捕獲的 lambda 能轉換成函式指標。
- 比較器寫成
>=/<=:給std::sort的比較器必須是嚴格弱序(相等時要回傳false)。寫a >= b是未定義行為,可能崩潰。 - lambda 寫太長:主體幾十行的 lambda 比具名函式更難讀。超過幾行就抽成函式。
練習題(至少 5 題)
基礎
練習 1:第一個 lambda(基礎)
用 lambda 寫出 square(平方)、isEven(判斷偶數)、maxOf(兩數取大),存進 auto 變數並在 main 測試。
提示:
auto isEven = [](int n) { return n % 2 == 0; };
練習 2:捕獲實驗(基礎)
建立 int base = 100;,分別寫傳值捕獲與傳參考捕獲的 lambda 各一個,都回傳 base + x。建立 lambda 之後把 base 改成 200 再呼叫兩者,觀察並解釋結果差異。
提示:傳值版印出的是建立當下的 100,傳參考版是最新的 200。
練習 3:計數器(基礎)
用「傳值捕獲 + mutable」做一個 counter lambda,每呼叫一次回傳 1、2、3、…。確認外部變數不受影響。
提示:
auto counter = [n = 0]() mutable { return ++n; };或先宣告int n = 0;再[n]() mutable。
中級
練習 4:成績統計(中級)
給定 std::vector<int> scores = {75, 42, 90, 58, 66, 88}; 與變數 int passing = 60;,用 lambda 搭配演算法完成:(1) count_if 算及格人數;(2) find_if 找出第一個不及格分數;(3) sort 由高到低排序。
提示:三個 lambda 中有兩個需要捕獲
passing。
練習 5:多鍵排序(中級)
建立 std::vector<std::string> words = {"banana", "kiwi", "apple", "fig"};,用 lambda 讓它「先依長度由短到長,長度相同再依字典序」排序。
提示:比較器內先比
a.size() != b.size(),相同時回傳a < b。
練習 6:自訂 filter(中級)
寫一個函式 std::vector<int> filter(const std::vector<int>& v, ??? pred),回傳所有讓 pred(x) 為 true 的元素。用泛型的方式接收 lambda(提示:把 filter 寫成模板,或參數型別直接用 auto——C++20),並分別用「偶數」和「大於 50」兩個 lambda 測試。
提示:
template <typename Pred> std::vector<int> filter(const std::vector<int>& v, Pred pred)。
挑戰
練習 7:懸垂捕獲偵探(挑戰) 寫一個函式回傳「捕獲了區域變數參考」的 lambda,呼叫它並觀察結果(可能是垃圾值、也可能碰巧正確——這正是 UB 的可怕之處)。再改成傳值捕獲修正它,說明兩者差異。
提示:開
-Wall -Wextra,部分編譯器會警告 dangling。
練習 8:簡易事件系統(挑戰)
用 std::vector 存放多個「無參數、無回傳」的 lambda(提示:先只存同一個 lambda 型別會遇到困難,思考為什麼;可先用函式指標 + 無捕獲 lambda 實作),依序觸發全部。這個練習的「不方便」正是 Ch15 std::function 要解決的問題。
提示:
std::vector<void(*)()> handlers; handlers.push_back([]{ std::cout << "事件1\n"; });
對應範例程式
| 檔案 | 說明 |
|---|---|
| lambda_basic.cpp | 基本語法、auto 儲存、回傳型別、IIFE、泛型 lambda |
| lambda_capture.cpp | 傳值/傳參考/混合/預設捕獲、mutable、懸垂陷阱示範 |
| lambda_algorithm.cpp | 與 sort、count_if、find_if、transform 等演算法合用 |
編譯與執行
g++ -std=c++17 -Wall -o lambda_basic lambda_basic.cpp && ./lambda_basic
g++ -std=c++17 -Wall -o lambda_capture lambda_capture.cpp && ./lambda_capture
g++ -std=c++17 -Wall -o lambda_algorithm lambda_algorithm.cpp && ./lambda_algorithm
延伸閱讀
- cppreference — Lambda expressions
- C++ Core Guidelines — F.50~F.53(何時用 lambda、如何捕獲)
- cppreference — Algorithms library
- 進階銜接:Ch15(Lambda 與函式物件)將在學完類別後,深入 lambda 的底層原理(閉包類別)、
std::function與函式物件(functor)。