Lambda 是「寫在表達式裡的匿名函式」。它讓你把一小段邏輯就地定義、就地使用,不必為了三行程式碼特地跑到別處寫一個具名函式。

在 Ch04 我們學會了函式,在 Ch06 認識了函式指標。Lambda 是這條路的自然延伸:它比函式指標更強大(可以「帶著狀態」),比具名函式更輕便(不必命名、不必宣告),是現代 C++ 中使用頻率極高的語言特性。


學習目標

  1. 理解 lambda 解決什麼問題:為什麼有了函式還需要 lambda
  2. 掌握 lambda 的完整語法:捕獲列表、參數、回傳型別、函式主體
  3. 深入理解捕獲(capture):傳值捕獲、傳參考捕獲、預設捕獲與其陷阱
  4. 理解 mutable 的意義與傳值捕獲的「副本」語意
  5. 學會用 auto 儲存 lambda,並認識泛型 lambda(auto 參數)
  6. 理解無捕獲 lambda 與函式指標的關係(銜接 Ch06)
  7. 能將 lambda 與陣列/std::vector 的走訪、排序、搜尋結合使用
  8. 認識懸垂捕獲(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)

對應 C++ Core Guidelines:

  1. F.50:當函式「用完即丟」或需要捕獲區域狀態時用 lambda;邏輯會被重複使用、或超過幾行時,寫成具名函式。
  2. F.52 / F.53:在當場使用、不會存活超過目前作用域的 lambda 中,傳參考捕獲沒問題;但若 lambda 會被儲存下來稍後呼叫(回呼、非同步),務必用傳值捕獲,避免懸垂參考。
  3. 明確捕獲優於預設捕獲:寫 [a, &b] 而不是 [=]、[&],讓依賴一目瞭然。
  4. lambda 主體保持短小(1~5 行為宜);太長代表它應該是一個具名函式。
  5. 存 lambda 一律用 auto;參數若是大型物件,跟一般函式一樣用 const T&。
  6. 善用 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 等演算法

常見錯誤與陷阱

  1. 懸垂參考捕獲(最危險):lambda 以 [&] 或 [&x] 捕獲區域變數,卻在該變數已銷毀後才被呼叫(例如把 lambda 回傳出去、存起來稍後執行)→ 未定義行為。會被存活下來的 lambda 請用傳值捕獲。
auto makeBad() {
    int local = 42;
    return [&local]() { return local; };  // ✗ 回傳後 local 已銷毀 → UB
}
  1. 以為傳值捕獲會「看到最新值」:傳值捕獲在 lambda 建立時就定格了,之後外部變數再怎麼改都無關。需要最新值請用參考捕獲。
  2. 忘記 mutable 就修改傳值捕獲:[n]() { n++; } 編譯錯誤;要嘛加 mutable(改副本),要嘛用 [&n](改本尊),先想清楚你要哪一種。
  3. 誤會 mutable 會改到外部變數:mutable 只解鎖「副本」的修改權,外部變數永遠不受影響。
  4. [=] 捕獲了大物件:預設傳值捕獲會複製所有用到的變數,若不小心用到大型 vector 就是一次昂貴的複製。明確捕獲、必要時用 const& 語意(傳參考捕獲且不修改)。
  5. 有捕獲的 lambda 硬塞給函式指標:只有無捕獲的 lambda 能轉換成函式指標。
  6. 比較器寫成 >= / <=:給 std::sort 的比較器必須是嚴格弱序(相等時要回傳 false)。寫 a >= b 是未定義行為,可能崩潰。
  7. 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

延伸閱讀