函式物件(functor)與 Lambda 表達式是 C++ 中實現「可呼叫物件(callable)」的兩大方式。它們讓你能把「行為」當作參數傳遞、儲存、組合,是使用 STL 演算法、設計回呼(callback)與策略模式不可或缺的工具。掌握 Lambda,等於拿到了現代 C++ 的萬用鑰匙。
學習目標
讀完本章,你應該能夠:
- 說明「可呼叫物件(callable)」的概念,列舉它的四種形式(函式、函式指標、函式物件、Lambda)
- 定義並使用函式物件(functor),包含有狀態的 functor 與謂詞 functor
- 完整拆解 Lambda 語法:捕獲列表、參數、
mutable、回傳型別、函式主體 - 精確掌握各種捕獲模式:
[=]、[&]、[this]、[x]、[&x]、[=, &x],以及 C++14 初始化捕獲[y = ...] - 使用泛型 Lambda(
auto參數,C++14)與mutableLambda - 解釋 Lambda 的本質——編譯器產生的閉包物件(closure),以及這對效能與生命週期的影響
- 使用
std::function統一儲存各種 callable,並理解其型別擦除與開銷 - 比較
std::bind與 Lambda,理解為何現代 C++ 優先用 Lambda - 把 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 演算法(sort、find_if、transform...)都接受「可呼叫物件」當參數,讓你把行為參數化。同一個 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)
- 優先用 Lambda 取代
std::bind(F.50/T.40 精神):更清楚、更易最佳化。 - 捕獲要精確:盡量列出需要的變數(
[x, &y]),少用包山包海的[=]/[&],以免意外捕獲或懸空。 - 小心參考捕獲的生命週期(F.53):會被儲存或非同步執行的 Lambda,不要用
[&]捕獲區域變數;改用值捕獲或移動捕獲。 - 成員函式裡的 Lambda 若會逃逸,用
[*this](C++17)複製,避免捕獲裸this懸空。 - 泛型 Lambda 參數用
const auto&避免不必要複製。 - 能用樣板參數就別用
std::function:保留型別、可內聯、零開銷;std::function留給「真的需要型別擦除」的場景。 - functor 的
operator()加上const(除非真的要改狀態):表達不修改的意圖。 - 需要狀態的就地行為用
mutableLambda;不需要狀態的小工具優先用無捕獲 Lambda(可轉函式指標)。
常見錯誤與陷阱
- 懸空參考捕獲:用
[&]捕獲區域變數後,Lambda 的壽命超過該變數(如作為回傳值、存進容器、丟給執行緒),呼叫時即懸空——未定義行為。 - 忘記
mutable:以值捕獲的變數預設const,嘗試在 Lambda 內修改會編譯錯誤。 - 誤以為
mutable會改到原變數:它改的是副本,外部原值不變。 std::function的效能開銷:型別擦除帶來間接呼叫與可能的動態配置;熱路徑請改用樣板參數。- 捕獲裸
this後物件被銷毀:成員 Lambda 逃逸後存取成員是 UB;用[*this]。 - Lambda 無法直接遞迴:需
std::function或自傳遞泛型 Lambda。 - 預定義 functor 忘記加
():std::greater<int>是型別,std::greater<int>()才是實例。 - 每個 Lambda 型別都不同:
auto a = []{}; auto b = []{};中a、b型別不同,不能互相賦值;要統一存放用std::function。 [=]在成員函式中會捕獲this(C++17 前):以為複製了成員,其實複製的是this指標,仍可能懸空。明確寫[*this]或具體成員。- 過度使用
[&]/[=]:隱式全捕獲讓人看不出 Lambda 依賴哪些變數,也容易意外抓到不該抓的東西。
重點整理
| 概念 | 要點 |
|---|---|
| 可呼叫物件 | 函式/函式指標/functor/Lambda,皆可用 obj(args) 呼叫 |
| 函式物件 | 重載 operator() 的類別實例,可攜帶狀態、易內聯 |
| 預定義 functor | std::plus、std::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會複製產生器,所以把同一實例的狀態觀察留意;也可改用mutableLambda[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、容器中的函式、事件系統、計算機、策略模式、函式組合 |