跳转至

Lambda 表達式

注意:考慮到算法競賽的實際情況,本文將不會全面研究語法,只會講述在算法競賽中可能會應用到的部分。

本文語法參照 C++11 標準。語義不同的將以 C++11 作為標準,C++14、C++17 的語法視情況提及並會特別標註。

Lambda 表達式

Lambda 表達式因數學中的 \(\lambda\) 演算得名,直接對應於其中的 lambda 抽象。Lambda 表達式能夠捕獲作用域中的變量的無名函數對象。我們可以將其理解為一個匿名的內聯函數,可以用來替換獨立函數或者函數對象,從而使代碼更可讀。但是從本質上來講,Lambda 表達式只是一種語法糖,因為它能完成的工作也可以用其他複雜的 C++ 語法來實現。

下面是 Lambda 表達式的語法:

1
[capture] (parameters) mutable -> return-type {statement}

下面我們分別對其中的 capture, parameters, mutable, return-type, statement 進行介紹。

capture 捕獲子句

Lambda 表達式以 capture 子句開頭,它指定哪些變量被捕獲,以及捕獲是通過值還是引用:有 & 符號前綴的變量通過引用訪問,沒有該前綴的變量通過值訪問。空的 capture 子句 [] 指示 Lambda 表達式的主體不訪問封閉範圍中的變量。

我們也可以使用默認捕獲模式:& 表示捕獲到的所有變量都通過引用訪問,= 表示捕獲到的所有變量都通過值訪問。之後我們可以為特定的變量 顯式 指定相反的模式。

例如 Lambda 體要通過引用訪問外部變量 a 並通過值訪問外部變量 b,則以下子句等效:

  • [&a, b]
  • [b, &a]
  • [&, b]
  • [b, &]
  • [=, &a]

默認捕獲時,會捕獲 Lambda 中提及的變量。獲的變量成為 Lambda 的一部分;與函數參數相比,調用 Lambda 時不必傳遞它們。

以下是一些常見的例子:

1
2
3
4
5
6
int a = 0;
auto f = []() { return a * 9; };   // Error, 無法訪問 'a'
auto f = [a]() { return a * 9; };  // OK, 'a' 被值「捕獲」
auto f = [&a]() { return a++; };   // OK, 'a' 被引用「捕獲」
                                  // 注意:請保證 Lambda 被調用時 a 沒有被銷燬
auto b = f();  // f 從捕獲列表裏獲得 a 的值,無需通過參數傳入 a

parameters 參數列表

大多數情況下類似於函數的參數列表,例如:

1
2
auto lam = [](int a, int b) { return a + b; };
std::cout << lam(1, 9) << " " << lam(2, 6) << std::endl;

C++14 中,若參數類型是泛型,則可以使用 auto 聲明類型:

1
auto lam = [](auto a, auto b)

一個例子:

1
2
3
int x[] = {5, 1, 7, 6, 1, 4, 2};
std::sort(x, x + 7, [](int a, int b) { return (a > b); });
for (auto i : x) std::cout << i << " ";

這將打印出 x 數組從大到小排序後的結果。

由於 parameters 參數列表 是可選的,如果不將參數傳遞給 Lambda 表達式,並且其 Lambda 聲明器不包含 mutable,且沒有後置返回值類型,則可以省略空括號。

Lambda 表達式也可以將另一個 Lambda 表達式作為其參數。

一個例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <functional>
#include <iostream>

int main() {
  using namespace std;

  // 返回另一個計算兩數之和 Lambda 表達式
  auto addtwointegers = [](int x) -> function<int(int)> {
    return [=](int y) { return x + y; };
  };

  // 接受另外一個函數 f 作為參數,返回 f(z) 的兩倍
  auto higherorder = [](const function<int(int)>& f, int z) {
    return f(z) * 2;
  };

  // 調用綁定到 higherorder 的 Lambda 表達式
  auto answer = higherorder(addtwointegers(7), 8);

  // 答案為 (7 + 8) * 2 = 15
  cout << answer << endl;
}

mutable 可變規範

利用可變規範,Lambda 表達式的主體可以修改通過值捕獲的變量。若使用此關鍵字,則 parameters 不可省略(即使為空)。

一個例子,使用 capture 捕獲字句 中的例子,來觀察 a 的值的變化:

1
2
int a = 0;
auto func = [a]() mutable { ++a; };

此時 lambda 中的 a 的值改變為 1,lambda 外的 a 保持不變。

return-type 返回類型

用於指定 Lambda 表達式的返回類型。若沒有指定返回類型,則返回類型將被自動推斷(行為與用 auto 聲明返回值的普通函數一致)。具體的,如果函數體中沒有 return 語句,返回類型將被推導為 void,否則根據返回值推導。若有多個 return 語句且返回值類型不同,將產生編譯錯誤。

例如,上文的 lam 也可以寫作:

1
auto lam = [](int a, int b) -> int

再舉兩個例子:

1
2
auto x1 = [](int i) { return i; };  // OK
auto x2 = [] { return {1, 2}; };    // Error, 返回類型被推導為 void

statement Lambda 主體

Lambda 主體可包含任何函數可包含的部分。普通函數和 Lambda 表達式主體均可訪問以下變量類型:

  • 從封閉範圍捕獲變量
  • 參數
  • 本地聲明的變量
  • 在一個 class 中聲明時,若捕獲 this,則可以訪問該對象的成員
  • 具有靜態存儲時間的任何變量,如全局變量

下面是一個例子

1
2
3
4
5
6
7
8
#include <iostream>

int main() {
  int m = 0, n = 0;
  [&, n](int a) mutable { m = (++n) + a; }(4);
  std::cout << m << " " << n << std::endl;
  return 0;
}

最後我們得到輸出 5 0。這是由於 n 是通過值捕獲的,在調用 Lambda 表達式後仍保持原來的值 0 不變。mutable 規範允許 n 在 Lambda 主體中被修改,將 mutable 刪去則編譯不通過。

使用類完成更復雜的操作

在 C++11 前沒有 Lambda 表達式,但可以使用稍複雜的方法替代,儘管看上去更復雜卻更易理解及擴展。

首先我們已經知道 Lambda 本質是一個可調用的對象,那麼直接定義一個類並構造一個對象,重載其 operator() 運算符就可以完成和 Lambda 一樣的操作,下面看一個簡單的例子,我們將使用 C++17 的語法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>

struct AbstractCallable {
  AbstractCallable(int v) : v_(v) {}

  virtual ~AbstractCallable() = default;
  virtual int operator()(int) const = 0;  // 純虛函數需要繼承後實現才可以實例化
  int v_;
};

AbstractCallable *get_CallableObject() {
  struct Callable : public AbstractCallable {
    Callable() : AbstractCallable(10) {}

    int operator()(int k) const override {
      std::cout << v_ + k << std::endl;
      return v_ - 10;
    }
  };

  return new Callable;
}

int main() {
  auto t = get_CallableObject();
  std::cout << t->operator()(5);  // 或者等價的 `(*t)(5);`
  delete t;
  return 0;
}

在寫 Lambda 表達式時,我們幾乎都可以將其等價的映射為上面這種形式。

Lambda 表達式相關語法 類的語法
capture 捕獲子句 構造函數
- 析構函數
使用 std::function 包裝傳遞 基類指針/引用傳遞
拷貝多個 Lambda 的函數對象 自定義的拷貝函數
mutable operator() 函數是否為 const

在 Lambda 的捕獲子句中分為引用捕獲和按值捕獲(暫不考慮比較特殊的捕獲 this 等),而在類的構造函數中我們可以更精細的控制這一點,另外自定義的析構函數的存在也方便我們更好的擴展,缺點是不夠「匿名」,因為仍需要類名。

假設我們有一個函數

1
void func(std::function<int(int)>);

那麼上述用例中就可以改為

1
void func(AbstractCallable *);

並且既然 Callable 是一個可調用對象,我們也可以通過 std::bind(&AbstractCallable::operator(), t, std::placeholders::_1) 來將其轉換再轉換為 std::function<int(int)>

如果不需要實現類似 std::function 的包裝,那麼也無需使用抽象基類,這樣便和一般的 Lambda 表達式一樣不會產生額外的虛擬函數表的開銷。

參考文獻