函式物件(functor)與 Lambda 表達式是 C++ 中實現「可呼叫物件(callable)」的兩大方式。它們讓你能把「行為」當作參數傳遞、儲存、組合,是使用 STL 演算法、設計回呼(callback)與策略模式不可或缺的工具。掌握 Lambda,等於拿到了現代 C++ 的萬用鑰匙。


學習目標

讀完本章,你應該能夠:

  1. 說明「可呼叫物件(callable)」的概念,列舉它的四種形式(函式、函式指標、函式物件、Lambda)
  2. 定義並使用函式物件(functor),包含有狀態的 functor 與謂詞 functor
  3. 完整拆解 Lambda 語法:捕獲列表、參數、mutable、回傳型別、函式主體
  4. 精確掌握各種捕獲模式:[=][&][this][x][&x][=, &x],以及 C++14 初始化捕獲 [y = ...]
  5. 使用泛型 Lambdaauto 參數,C++14)與 mutable Lambda
  6. 解釋 Lambda 的本質——編譯器產生的閉包物件(closure),以及這對效能與生命週期的影響
  7. 使用 std::function 統一儲存各種 callable,並理解其型別擦除開銷
  8. 比較 std::bind 與 Lambda,理解為何現代 C++ 優先用 Lambda
  9. 把 callable 傳給演算法、存進容器、實作遞迴 Lambda,以及 C++14 以移動方式捕獲

一、可呼叫物件(Callable)這個統一概念(WHY)

在 C++ 裡,凡是能用 obj(args...) 語法呼叫的東西,都叫可呼叫物件。它有四種形式:

形式 範例 能攜帶狀態?
普通函式 int add(int,int)
函式指標 int (*p)(int,int) = add;
函式物件(functor) 重載 operator() 的類別實例
Lambda 表達式 [](int x){ return x*2; } ✅(透過捕獲)

為什麼需要這個抽象? 因為 STL 演算法(sortfind_iftransform...)都接受「可呼叫物件」當參數,讓你把行為參數化。同一個 std::sort,傳不同的比較器就有不同的排序行為——這正是「策略」可插拔的精髓。Lambda 之所以重要,是因為它讓你能在呼叫點就地定義這個行為,不必跑到別處宣告一個具名函式。


二、函式物件(Functor)

函式物件是一個重載了 operator() 的類別實例,使其能像函式一樣被呼叫。

2.1 基本範例

class Adder {
    int offset_;
public:
    explicit Adder(int n) : offset_(n) {}
    int operator()(int x) const { return x + offset_; }
};

Adder add5(5);
std::cout << add5(10);   // 輸出 15

2.2 為什麼用函式物件而不是普通函式?

特性 普通函式 函式物件
狀態 無(或靠全域變數,難維護) 可攜帶狀態(成員變數)
參數化 行為固定 建構子可注入參數(如上例的 offset_
內聯最佳化 透過函式指標呼叫,編譯器難內聯 型別在編譯期已知,容易內聯,通常更快
可複用 同一個 functor 可建立多個帶不同狀態的實例

效能洞察:把 functor 或 Lambda 傳給 std::sort,通常比傳函式指標快,因為前者的呼叫可在編譯期被內聯,後者往往得透過指標間接呼叫。

2.3 有狀態的函式物件(Stateful Functor)

functor 能在多次呼叫之間「記住」資訊:

class Counter {
    int count_ = 0;
public:
    void operator()(int /*ignored*/) { ++count_; }
    int getCount() const { return count_; }
};

std::vector<int> v = {10, 20, 30, 40, 50};
// for_each 會回傳 functor 的副本,可藉此取得最終狀態
Counter c = std::for_each(v.begin(), v.end(), Counter());
std::cout << c.getCount();   // 5

2.4 謂詞函式物件(Predicate Functor)

回傳 bool 的 functor 稱為謂詞,常用於條件判斷。建構子注入的參數讓同一個 functor 產生不同條件:

class InRange {
    int lo_, hi_;
public:
    InRange(int lo, int hi) : lo_(lo), hi_(hi) {}
    bool operator()(int x) const { return x >= lo_ && x <= hi_; }
};

std::vector<int> scores = {45, 72, 88, 91, 63};
int bCount = std::count_if(scores.begin(), scores.end(), InRange(80, 89));  // 1

三、預定義函式物件(<functional>

標準庫已備好常用的 functor,省得你重寫:

3.1 算術與比較

算術 運算 比較 運算
std::plus<T> a + b std::equal_to<T> a == b
std::minus<T> a - b std::not_equal_to<T> a != b
std::multiplies<T> a * b std::greater<T> a > b
std::divides<T> a / b std::less<T> a < b
std::modulus<T> a % b std::greater_equal<T> a >= b
std::negate<T> -a std::less_equal<T> a <= b
#include <functional>
std::vector<int> v = {5, 2, 8, 1, 9};
std::sort(v.begin(), v.end(), std::greater<int>());          // 降序
int prod = std::accumulate(v.begin(), v.end(), 1, std::multiplies<int>());

C++14 透明運算子:用 std::greater<>()(不指定型別),讓型別由參數自動推導,泛型程式更方便。


四、Lambda 表達式:完整語法拆解

Lambda 是 C++11 引入的匿名函式語法。完整形式:

[capture] (params) specifiers -> return_type { body }
   │          │         │           │           └─ 函式主體(必要)
   │          │         │           └─ 回傳型別(可省略,自動推導)
   │          │         └─ mutable / noexcept / constexpr 等修飾(可選)
   │          └─ 參數列表(無參數可省略括號)
   └─ 捕獲列表(必要,可為空 [])
部分 說明 是否必要
[capture] 捕獲外部變數,這是 Lambda 與普通函式最大的差別 必要(可空 []
(params) 參數列表 無參數時可省略
mutable 允許修改以值捕獲的副本 可選
-> return_type 指定回傳型別 可選(通常自動推導)
{ body } 函式主體 必要
auto hello  = []{ std::cout << "Hi\n"; };               // 最簡:無捕獲、無參數
auto add    = [](int a, int b){ return a + b; };          // 帶參數
auto divide = [](double a, double b) -> double {          // 顯式回傳型別
                  return b == 0 ? 0.0 : a / b;
              };
int r = [](int x){ return x * x; }(5);                     // IIFE:定義後立刻呼叫,r = 25

五、捕獲模式(Capture)——Lambda 的核心

捕獲列表決定 Lambda 如何存取「定義它的作用域」中的變數。

5.1 各種捕獲方式

int x = 10, y = 20;

auto f1 = []      { /* 不捕獲 */ };
auto f2 = [=]     { return x + y; };   // 以「值」捕獲全部(複製快照)
auto f3 = [&]     { x++; y++; };       // 以「參考」捕獲全部(操作原變數)
auto f4 = [x]     { return x * 2; };   // 只以值捕獲 x
auto f5 = [&x]    { x = 100; };        // 只以參考捕獲 x
auto f6 = [=, &x] { x = y + 1; };      // 預設值捕獲,x 例外用參考
auto f7 = [&, y]  { x = y + 1; };      // 預設參考捕獲,y 例外用值

5.2 值捕獲 vs 參考捕獲(關鍵差異)

int n = 10;
auto byValue = [n]{ return n; };   // 在「定義時」拍下快照 n=10
auto byRef   = [&n]{ return n; };  // 綁定到變數本身
n = 999;
std::cout << byValue();  // 10  ← 快照不受後續修改影響
std::cout << byRef();    // 999 ← 反映最新值
捕獲語法 含義 能改到原變數?
[] 不捕獲
[=] 全部以值捕獲 否(除非 mutable,且改的是副本)
[&] 全部以參考捕獲
[x] x 以值捕獲
[&x] x 以參考捕獲
[=, &x] 預設值捕獲,x 例外參考 只有 x
[this] 捕獲當前物件指標,可存取成員 是(透過 this)

5.3 捕獲 this*this

在成員函式中,Lambda 要存取成員必須捕獲 this

class Widget {
    int value_ = 42;
public:
    auto byPointer() { return [this]  { return value_; }; }  // 捕獲指標,共用同一物件
    auto byCopy()    { return [*this] { return value_; }; }  // C++17:捕獲 *this 的副本
};

危險[this] 只捕獲指標。若 Lambda 的壽命超過物件(例如存進容器、非同步回呼),物件銷毀後再呼叫即懸空存取。C++17 的 [*this] 複製整個物件,較安全。

5.4 ⭐ 初始化捕獲(Init-capture,C++14)

C++14 允許在捕獲列表中就地建立新變數,語法 [名稱 = 運算式]。它解決兩個問題:(1) 捕獲計算結果;(2) 以移動方式捕獲(見第十節)。

int base = 10;
auto f = [doubled = base * 2](int x){ return x + doubled; };  // 捕獲計算結果 20
std::cout << f(5);   // 25

// 以移動捕獲(unique_ptr 無法複製,只能移動)
auto ptr = std::make_unique<int>(99);
auto g = [p = std::move(ptr)]{ return *p; };
std::cout << g();    // 99

六、mutable Lambda

以值捕獲的變數在 Lambda 內預設為 const(因為 operator() 預設是 const 成員)。加上 mutable 才能修改那份副本

int seed = 0;
auto gen = [seed]() mutable { return ++seed; };  // 修改的是副本
std::cout << gen() << gen() << gen();  // 1 2 3(副本累加,有狀態)
std::cout << seed;                     // 0(原始值不變)

mutable 讓值捕獲的 Lambda 變成「有狀態的小機器」,常用來做 ID 產生器、計數器。它修改的永遠是內部副本,外部原變數不受影響。


七、泛型 Lambda(Generic Lambda,C++14)

參數型別用 auto,Lambda 就變成一個「樣板化」的可呼叫物件,能接受不同型別:

auto print = [](const auto& x){ std::cout << x << "\n"; };
print(42);            // int
print(3.14);          // double
print("hello");       // const char*

auto add = [](auto a, auto b){ return a + b; };
std::cout << add(1, 2);       // 3
std::cout << add(1.5, 2.5);   // 4.0

最佳實踐:泛型 Lambda 的 auto 參數遵循樣板推導規則。對可能較大的型別用 const auto& 避免不必要的複製。C++20 更進一步支援「樣板語法 Lambda」[]<typename T>(T x){...}


八、Lambda 的本質:閉包物件(Closure)

理解 Lambda 不是魔法的關鍵:編譯器看到 Lambda,會自動產生一個獨一無二的匿名類別(closure type),並建立它的一個實例(closure object)。捕獲的變數成為該類別的成員,Lambda 主體成為它的 operator()

// 你寫的:
int factor = 3;
auto mul = [factor](int x){ return x * factor; };

// 編譯器大致產生的等價物:
class __Lambda_xyz {
    int factor;                                  // 捕獲 → 成員變數
public:
    __Lambda_xyz(int f) : factor(f) {}
    int operator()(int x) const { return x * factor; }  // 主體 → operator()
};
auto mul = __Lambda_xyz(factor);

由此可推出幾個重要事實:

  • 每個 Lambda 都有獨一無二的型別(即使簽章相同)——所以才需要 auto 來接,或用 std::function 統一型別。
  • 沒有捕獲的 Lambda 可隱式轉成「函式指標」;有捕獲的不行(因為它帶著狀態)。
  • 捕獲就是「初始化成員」,因此值捕獲是複製、參考捕獲是綁定。
  • 因為型別在編譯期已知,呼叫可被內聯,效能通常等同手寫 functor。

九、std::function:型別擦除的統一包裝

每個 Lambda 型別都不同,無法直接宣告「一個能存任何 int(int) Lambda 的變數」。std::function(在 <functional>)透過型別擦除(type erasure)解決此問題:它能存放任何符合簽章的 callable。

9.1 基本用法

#include <functional>
std::function<int(int, int)> op;

op = [](int a, int b){ return a + b; };   // 存 Lambda
std::cout << op(3, 4);                      // 7
op = std::plus<int>();                      // 存 functor
int multiply(int, int);
op = multiply;                              // 存普通函式

std::function<void()> empty;
if (!empty) std::cout << "尚未綁定\n";       // 可用 if 檢查是否為空

9.2 三大用途

// (1) 作為函式參數(callback)
void apply(const std::vector<int>& v, std::function<int(int)> f) {
    for (int x : v) std::cout << f(x) << " ";
}
apply({1,2,3}, [](int x){ return x * x; });   // 1 4 9

// (2) 存進容器(如「指令表」)
std::map<std::string, std::function<double(double,double)>> ops = {
    {"+", [](double a, double b){ return a + b; }},
    {"-", [](double a, double b){ return a - b; }},
    {"*", [](double a, double b){ return a * b; }},
};
std::cout << ops["*"](4, 5);   // 20

// (3) 策略模式:執行期切換行為
using Discount = std::function<double(double)>;
Discount strategy = [](double p){ return p * 0.8; };

9.3 開銷與取捨

std::function 很方便,但不是零成本:

面向 說明
動態配置 較大的 callable 可能堆積配置(小型有 SBO 小物件最佳化,視實作)
間接呼叫 透過型別擦除,呼叫無法內聯,類似虛擬函式的間接成本
何時用 需要把不同型別的 callable 存在同一變數/容器,或穿越 API 邊界時
何時避免 對效能敏感的內層迴圈、能用樣板參數接 callable 時

最佳實踐:函式樣板參數(template <class F> void f(F cb))能保留具體型別、可內聯、零開銷;只有在「真的需要型別擦除」時才用 std::function


十、std::bind vs Lambda:優先用 Lambda

C++11 的 std::bind 可以「綁定部分參數」產生新 callable,但現代 C++ 幾乎都能用 Lambda 取代,且更清楚:

#include <functional>
int add(int a, int b) { return a + b; }

// std::bind 寫法(需要 placeholders,較難讀)
using namespace std::placeholders;
auto add10_bind = std::bind(add, 10, _1);

// Lambda 寫法(清楚直觀,現代首選)
auto add10_lambda = [](int x){ return add(10, x); };

std::cout << add10_bind(5) << " " << add10_lambda(5);  // 15 15
比較點 std::bind Lambda
可讀性 需理解 _1_2 佔位符,較晦澀 直接,貼近一般函式
最佳化 較難內聯 易內聯
彈性 重排參數較麻煩 任意組合、加邏輯都容易
C++ Core Guidelines F.50/T.40 精神:優先用 Lambda

結論:除非有特殊理由,一律用 Lambda,不用 std::bind


十一、遞迴 Lambda

Lambda 沒有名字,無法在主體內直接呼叫「自己」。有兩種常見解法:

11.1 用 std::function(簡單但有開銷)

#include <functional>
std::function<int(int)> factorial = [&](int n){
    return n <= 1 ? 1 : n * factorial(n - 1);
};
std::cout << factorial(5);   // 120

11.2 泛型 Lambda 自傳遞(Y-combinator 風格,無 std::function 開銷)

auto factorial = [](auto&& self, int n) -> int {
    return n <= 1 ? 1 : n * self(self, n - 1);
};
std::cout << factorial(factorial, 5);   // 120

C++23 起有 this auto 的「deducing this」可讓 Lambda 直接遞迴,但在 C++17 環境下,上述兩種是標準做法。第一種好讀、第二種免去 std::function 的型別擦除成本。


十二、以移動方式捕獲(Capture by Move,C++14)

對於只能移動、不能複製的資源(如 std::unique_ptr、大型 buffer),用初始化捕獲以移動方式帶入 Lambda:

auto data = std::make_unique<std::vector<int>>(std::vector<int>{1,2,3});
auto task = [d = std::move(data)]{
    int sum = 0;
    for (int x : *d) sum += x;
    return sum;
};
std::cout << task();   // 6
// 此時外部的 data 已被移走(為 nullptr)

這在把工作打包丟給執行緒、std::async、或事件佇列時極為常用——把資源的所有權「搬進」Lambda,避免複製與懸空。


最佳實踐(C++ Core Guidelines)

  1. 優先用 Lambda 取代 std::bind(F.50/T.40 精神):更清楚、更易最佳化。
  2. 捕獲要精確:盡量列出需要的變數([x, &y]),少用包山包海的 [=]/[&],以免意外捕獲或懸空。
  3. 小心參考捕獲的生命週期(F.53):會被儲存或非同步執行的 Lambda,不要用 [&] 捕獲區域變數;改用值捕獲或移動捕獲。
  4. 成員函式裡的 Lambda 若會逃逸,用 [*this](C++17)複製,避免捕獲裸 this 懸空。
  5. 泛型 Lambda 參數用 const auto& 避免不必要複製。
  6. 能用樣板參數就別用 std::function:保留型別、可內聯、零開銷;std::function 留給「真的需要型別擦除」的場景。
  7. functor 的 operator() 加上 const(除非真的要改狀態):表達不修改的意圖。
  8. 需要狀態的就地行為mutable Lambda;不需要狀態的小工具優先用無捕獲 Lambda(可轉函式指標)。

常見錯誤與陷阱

  1. 懸空參考捕獲:用 [&] 捕獲區域變數後,Lambda 的壽命超過該變數(如作為回傳值、存進容器、丟給執行緒),呼叫時即懸空——未定義行為。
  2. 忘記 mutable:以值捕獲的變數預設 const,嘗試在 Lambda 內修改會編譯錯誤。
  3. 誤以為 mutable 會改到原變數:它改的是副本,外部原值不變。
  4. std::function 的效能開銷:型別擦除帶來間接呼叫與可能的動態配置;熱路徑請改用樣板參數。
  5. 捕獲裸 this 後物件被銷毀:成員 Lambda 逃逸後存取成員是 UB;用 [*this]
  6. Lambda 無法直接遞迴:需 std::function 或自傳遞泛型 Lambda。
  7. 預定義 functor 忘記加 ()std::greater<int>型別std::greater<int>() 才是實例
  8. 每個 Lambda 型別都不同auto a = []{}; auto b = []{};ab 型別不同,不能互相賦值;要統一存放用 std::function
  9. [=] 在成員函式中會捕獲 this(C++17 前):以為複製了成員,其實複製的是 this 指標,仍可能懸空。明確寫 [*this] 或具體成員。
  10. 過度使用 [&]/[=]:隱式全捕獲讓人看不出 Lambda 依賴哪些變數,也容易意外抓到不該抓的東西。

重點整理

概念 要點
可呼叫物件 函式/函式指標/functor/Lambda,皆可用 obj(args) 呼叫
函式物件 重載 operator() 的類別實例,可攜帶狀態、易內聯
預定義 functor std::plusstd::greater 等(<functional>);C++14 透明版 std::greater<>()
Lambda 語法 [capture](params) mutable -> ret { body }
捕獲模式 [=]值、[&]參考、[x]/[&x]指定、[this]/[*this][y=...]初始化捕獲(C++14)
mutable 讓值捕獲的副本可改(不影響原變數)
泛型 Lambda auto 參數(C++14),等同樣板化
閉包本質 編譯器產生匿名類別+實例;捕獲=成員,主體=operator();型別唯一
std::function 型別擦除統一包裝,有間接呼叫/配置開銷
bind vs Lambda 優先 Lambda(清楚、可內聯)
遞迴 Lambda std::function 或自傳遞泛型 Lambda
移動捕獲 [p = std::move(x)](C++14),搬移資源所有權

練習題

每題附難度與提示。建議先自己動手,再對照 對應範例程式。所有程式須能以 g++ -std=c++17 -Wall 編譯。

練習 1(基礎):計數器函式物件

設計一個 Counter 函式物件,每次呼叫 operator() 回傳從 1 開始遞增的整數。用 std::generate 把一個 std::vector<int>(10) 填成 1..10,並輸出驗證。

  • 提示:Counter 需有一個成員 int n_ = 0;operator() 回傳 ++n_std::generate(v.begin(), v.end(), counterInstance)。注意 generate 會複製產生器,所以把同一實例的狀態觀察留意;也可改用 mutable Lambda [n=0]() mutable { return ++n; }

練習 2(基礎):閾值篩選器工廠

寫一個函式(或 Lambda)makeGreaterThan(int t),回傳一個「判斷輸入是否大於 t」的 Lambda。用它搭配 std::count_if 算出某 vector 中大於 50 的元素個數。

  • 提示:auto makeGreaterThan = [](int t){ return [t](int x){ return x > t; }; };(Lambda 回傳 Lambda,內層用值捕獲 t)。

練習 3(中級):簡易計算機

std::map<std::string, std::function<double(double,double)>> 實作支援 + - * / 的計算機。讀入「運算子 運算元1 運算元2」,查表執行並輸出結果;除以零要處理。

  • 提示:把每個運算子對應到一個 Lambda 存進 map。查表 auto it = ops.find(symbol); if (it != ops.end()) result = it->second(a, b);

練習 4(中級):學生多鍵排序

有一組學生 struct Student { std::string name; int score; }。用 Lambda 實作兩種排序:(a) 按分數降序;(b) 分數相同時按姓名字典序升序。輸出兩種結果比較差異。

  • 提示:比較 lambda:[](const Student& a, const Student& b){ if (a.score != b.score) return a.score > b.score; return a.name < b.name; }。傳給 std::sort

練習 5(中級):函式組合 compose

寫一個 compose 函式,接受兩個 std::function<int(int)> f、g,回傳新函式 h(x) = f(g(x))。驗證 compose(加10, 乘2)(5) == 20

  • 提示:auto compose = [](std::function<int(int)> f, std::function<int(int)> g){ return [f, g](int x){ return f(g(x)); }; };。注意內層 Lambda 要以值捕獲 f、g。

練習 6(挑戰):遞迴 Lambda 求費氏數列

不使用具名函式,分別用 (a)「std::function 遞迴」與 (b)「自傳遞泛型 Lambda(Y-combinator 風格)」計算費氏數列第 n 項 fib(n),並比較兩種寫法。

  • 提示:(a) std::function<int(int)> fib = [&](int n){ return n < 2 ? n : fib(n-1) + fib(n-2); };。(b) auto fib = [](auto&& self, int n) -> int { return n < 2 ? n : self(self, n-1) + self(self, n-2); };,呼叫 fib(fib, n)

練習 7(挑戰):以移動捕獲打包任務

建立一個 std::unique_ptr<std::vector<int>> 指向一組數字,寫一個 Lambda 用初始化捕獲以移動方式接管它,並在 Lambda 內計算總和回傳。把多個這樣的 Lambda 存進 std::vector<std::function<int()>> 後依序執行。

  • 提示:[p = std::move(ptr)]() { int s = 0; for (int x : *p) s += x; return s; }。移動後外部指標變 nullptr,不要再用它。

對應範例程式

檔案 說明
lambda.cpp Lambda 語法、值/參考/混合捕獲、mutable、泛型 Lambda、回傳 Lambda、搭配 sort/find_if/transform
function_objects.cpp 函式物件、有狀態 functor、謂詞 functor、比較器、預定義 functor、三種 callable 比較
std_function.cpp std::function 基本用法、callback、容器中的函式、事件系統、計算機、策略模式、函式組合