第 22.11 節

Lambda 表達式

0瀏覽次數0訪問次數--跳出率--平均停留

本節解決什麼問題

很多地方需要傳入一小段邏輯:排序規則、過濾條件、按鈕回調、定時器回調。舊寫法通常有三種:

  1. 寫一個普通函數。
  2. 寫一個函數指針。
  3. 寫一個函數對象,也就是帶 operator() 的類或結構體。

這些寫法都能工作,但小邏輯會被迫挪到遠處,或者要額外寫一個類型。Lambda 表達式讓你可以在使用現場直接寫一個小函數,而且可以捕獲外部變量。

這個特性是什麼

Lambda 是匿名可調用對象。它看起來像一個函數,但本質上是編譯器生成的一個類對象。

基本結構是:

部分例子含義
捕獲列表[x, &y]從外部作用域拿哪些變量
參數列表(int a, int b)調用 lambda 時傳入什麼
返回值-> int返回類型,通常可以省略
函數體{ return a + b; }真正執行的代碼

完整形式可以寫成 [x](int n) -> int { return x + n; },常見情況下返回值類型可以省略。

C++ 標準版本

  • C++11:基礎 Lambda。
  • C++14:泛型 Lambda,也就是參數可以寫 auto
  • C++17:constexpr Lambda、[*this] 捕獲。

Lambda 是語言特性,不需要額外頭文件。只有配合 STL 算法、std::function 等庫工具時,才需要包含對應頭文件。

捕獲列表速查

寫法含義適用場景
[]不捕獲外部變量只用參數或局部臨時變量
[x]按值捕獲 x保存一份副本,適合保存回調
[&x]按引用捕獲 x需要修改外部變量,且能保證生命週期
[x, &y]混合捕獲明確表達哪些拷貝、哪些引用
[=]默認按值捕獲用到的變量小例子方便,工程中不建議濫用
[&]默認按引用捕獲用到的變量異步或保存回調中尤其危險
[this]捕獲當前對象指針對象必須比 lambda 活得久
[*this]捕獲當前對象副本C++17 起可用,適合避免懸空 this

示例代碼

示例 1:舊回調寫法和 lambda 的區別

普通函數和函數對象都可以作爲算法條件,但 Lambda 更適合寫簡短的局部邏輯。

#include <algorithm>
#include <iostream>
#include <vector>

bool is_even(int n)
{
    return n % 2 == 0;
}

struct GreaterThan
{
    int limit;

    bool operator()(int n) const
    {
        return n > limit;
    }
};

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // vector 是动态数组,元素数量可以在运行时变化。
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6};

    int even_count = std::count_if(numbers.begin(), numbers.end(), is_even);
    std::cout << "even count = " << even_count << "\n";

    int greater_count1 = std::count_if(numbers.begin(), numbers.end(), GreaterThan{3});
    std::cout << "> 3 count (functor) = " << greater_count1 << "\n";

    int limit = 3;
    int greater_count2 = std::count_if(numbers.begin(), numbers.end(),
                                       [limit](int n) {
                                           return n > limit;
                                       });
    std::cout << "> 3 count (lambda) = " << greater_count2 << "\n";

    return 0;
}

運行結果

even count = 3
> 3 count (functor) = 3
> 3 count (lambda) = 3

count_if 的第三個參數是什麼?

先看這一句:

int even_count = std::count_if(numbers.begin(), numbers.end(), is_even);

std::count_if 可以簡單理解爲:

從指定範圍中逐個取出元素,調用你提供的“判斷規則”,統計其中返回 true 的元素有多少個。

它的基本形式是:

std::count_if(起点, 终点, 判断规则);

第三個參數就是這個“判斷規則”。它並不一定非得是普通函數,只要是可以像函數一樣調用的對象就可以,包括普通函數、函數對象和 Lambda。這些東西統稱爲可調用對象(Callable)

這裏第三個參數傳入的是:

is_even

is_even 是:

bool is_even(int n)
{
    return n % 2 == 0;
}

count_if 會把 numbers 中的元素一個一個傳給它,過程可以近似理解成:

is_even(1);  // false,不计数
is_even(2);  // true,计数 +1
is_even(3);  // false,不计数
is_even(4);  // true,计数 +1
is_even(5);  // false,不计数
is_even(6);  // true,计数 +1

因此最後得到 3

可以把 count_if 的內部工作近似理解成:

int count = 0;

for (每一个元素)
{
    if (判断规则(当前元素))
    {
        ++count;
    }
}

也就是說,count_if 自己負責:

  • 怎麼遍歷容器;
  • 怎麼統計數量。

第三個參數負責告訴它:

什麼樣的元素纔算符合條件?

同一個位置也可以傳入函數對象:

std::count_if(numbers.begin(), numbers.end(), GreaterThan{3});

GreaterThan{3} 可以像函數一樣被調用,因爲結構體定義了:

bool operator()(int n) const
{
    return n > limit;
}

它回答的問題就是:

當前這個 n 是否大於 3

還可以直接傳 Lambda:

std::count_if(numbers.begin(), numbers.end(),
              [limit](int n) {
                  return n > limit;
              });

count_if 同樣會反覆調用這個 Lambda:

lambda(1);  // false
lambda(2);  // false
lambda(3);  // false
lambda(4);  // true
lambda(5);  // true
lambda(6);  // true

所以這三種寫法雖然形式不同,本質上都是在給 count_if 提供一個判斷規則:

is_even
GreaterThan{3}
[limit](int n) { return n > limit; }

Lambda 的優勢在於:如果這個判斷規則只在這裏使用一次,就可以直接寫在算法旁邊,不需要額外定義一個函數或結構體。

示例 2:最基本的 lambda 語法、參數和返回值

#include <iostream>
#include <string>

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    auto add = [](int a, int b) {
        return a + b;
    };

    auto describe_score = [](int score) -> std::string {
        if (score >= 60)
        {
            return "pass";
        }
        return "fail";
    };

    std::cout << "add(3, 5) = " << add(3, 5) << "\n";
    std::cout << "score 80 is " << describe_score(80) << "\n";
    std::cout << "score 40 is " << describe_score(40) << "\n";

    // 返回 0 表示程序正常结束。
    return 0;
}

運行結果

add(3, 5) = 8
score 80 is pass
score 40 is fail

示例 3:按值捕獲和按引用捕獲

按值捕獲會保存定義 lambda 時的副本;按引用捕獲會訪問外部變量本身。

#include <iostream>

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    int score = 10;

    auto add_by_value = [score](int bonus) {
        return score + bonus;
    };

    auto add_by_ref = [&score](int bonus) {
        score += bonus;
        return score;
    };

    score = 20;

    std::cout << "value capture result = " << add_by_value(5) << "\n";
    std::cout << "ref capture result = " << add_by_ref(5) << "\n";
    std::cout << "score after ref capture = " << score << "\n";

    // 返回 0 表示程序正常结束。
    return 0;
}

運行結果

value capture result = 15
ref capture result = 25
score after ref capture = 25

示例 4:mutable 允許修改按值捕獲的副本

按值捕獲的變量默認在 lambda 內是隻讀的。加上 mutable 後,可以修改 lambda 自己保存的副本,但不會修改外部變量。

#include <iostream>

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    int start = 0;

    auto counter = [start]() mutable {
        ++start;
        return start;
    };

    std::cout << "counter() = " << counter() << "\n";
    std::cout << "counter() = " << counter() << "\n";
    std::cout << "outside start = " << start << "\n";

    // 返回 0 表示程序正常结束。
    return 0;
}

運行結果

counter() = 1
counter() = 2
outside start = 0

爲什麼連續調用 counter() 會繼續累加?

這裏不是因爲外部 start 的作用域變大了,而是因爲 Lambda 對象 counter 自己保存了一份 start

int start = 0;

auto counter = [start]() mutable {
    ++start;
    return start;
};

[start] 是按值捕獲。可以近似理解爲編譯器生成了這樣的對象:

struct Counter
{
    int start;

    int operator()()
    {
        ++start;
        return start;
    }
};

Counter counter{start};

因此程序中實際上有兩個不同的 start

  • 外部 start:始終還是 0
  • counter 內部保存的 start:第一次調用變成 1,第二次調用繼續變成 2
counter();  // 内部 start:0 -> 1
counter();  // 内部 start:1 -> 2

關鍵在於:

auto counter = [start]() mutable { ... };

這裏只創建了一次 Lambda 對象。後面的兩個 counter() 都是在調用同一個對象,不會每次調用時重新捕獲一次外部 start

mutable 的作用則是允許修改 Lambda 內部按值捕獲的副本,它並不會讓外部的 start 一起變化。

示例 5:lambda 配合 STL 算法

Lambda 和 STL 算法配合時最常見:排序、查找、計數、轉換都可以把局部邏輯直接寫在調用處。

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // vector 是动态数组,元素数量可以在运行时变化。
    std::vector<std::string> names = {"Bob", "Alice", "Charlie", "David"};

    std::sort(names.begin(), names.end(),
              [](const std::string& a, const std::string& b) {
                  if (a.size() == b.size())
                  {
                      return a < b;
                  }
                  return a.size() < b.size();
              });

    std::cout << "sort by length: ";
    for (const auto& name : names)
    {
        std::cout << name << " ";
    }
    std::cout << "\n";

    int min_length = 6;
    auto it = std::find_if(names.begin(), names.end(),
                           [min_length](const std::string& name) {
                               return name.size() >= static_cast<std::size_t>(min_length);
                           });

    if (it != names.end())
    {
        std::cout << "first long name = " << *it << "\n";
    }

    return 0;
}

運行結果

sort by length: Bob Alice David Charlie
first long name = Charlie

示例中使用的 API 和語法

這個示例除了 Lambda,還使用了一些常見的 STL API。

寫法簡單說明
std::vector<std::string>動態數組,這裏用於保存多個字符串
names.begin()返回指向第一個元素的迭代器
names.end()返回最後一個元素後一個位置的迭代器
std::sort(begin, end, rule)對指定範圍排序,第三個參數指定比較規則
string.size()返回字符串長度
const std::string&只讀引用字符串,避免額外複製
for (const auto& x : container)範圍 for,依次訪問容器中的每個元素
auto讓編譯器自動推導變量類型
std::find_if(begin, end, rule)查找第一個滿足條件的元素
static_cast<T>(value)value 顯式轉換爲類型 T
std::size_tSTL 中常用於表示大小、數量和下標的無符號整數類型
*it取得迭代器 it 當前指向的元素

其中 begin()end() 經常一起出現:

names.begin(), names.end()

表示整個 names 的遍歷範圍。需要注意,end() 並不指向最後一個元素,而是最後一個元素後面的結束位置:

Bob    Alice    David    Charlie    [结束位置]
 ↑                                      ↑
begin()                                end()

STL 算法爲什麼要接收一個“規則函數”?

std::count_ifstd::find_ifstd::sort 有一個共同特點:

算法自己負責“怎麼做”,你提供的函數負責“按照什麼規則做”。

例如 std::sort 自己知道排序算法應該怎樣移動和比較元素,但是它不知道你希望按照數值大小、字符串長度還是其他規則排序。

同樣,std::find_if 自己知道怎樣從前往後查找,但是它不知道“什麼樣的元素纔算你要找的元素”。

因此這些算法會接收一個可調用對象,並在運行過程中反覆調用它。

在示例 1 中已經看到:

std::count_if(begin, end, rule);

它會對每個元素調用:

rule(element);

第三個參數回答的是:

這個元素算不算?

find_ifsort 使用的是同一種設計思想,只是它們向規則函數提出的問題不同。

std::find_if:這個元素是不是我要找的?

基本形式是:

std::find_if(起点, 终点, 判断规则);

示例中:

auto it = std::find_if(names.begin(), names.end(),
                       [min_length](const std::string& name) {
                           return name.size() >= static_cast<std::size_t>(min_length);
                       });

[min_length] 按值捕獲最小長度 6。這個 Lambda 接收一個字符串,並判斷它的長度是否至少爲 6

find_if 會依次調用這個 Lambda,可以近似理解成:

rule("Bob");      // false
rule("Alice");    // false
rule("David");    // false
rule("Charlie");  // true

一旦第一次得到 truefind_if 就停止查找,並返回當前元素的位置。

因此 find_if 的第三個參數回答的是:

當前元素是不是我要找的?

它和 count_if 的規則函數形式很像,通常都是:

bool rule(一个元素);

區別在於算法得到 true 之後做什麼:

算法規則返回 true 後的行爲
count_if計數加一,然後繼續檢查後面的元素
find_if已經找到,立即停止並返回當前位置

std::sorta 應不應該排在 b 前面?

std::sort 稍微不同:

std::sort(起点, 终点, 比较规则);

它的第三個參數一次接收兩個元素

[](const std::string& a, const std::string& b) {
    if (a.size() == b.size())
    {
        return a < b;
    }
    return a.size() < b.size();
}

sort 會在排序過程中不斷調用:

rule(a, b);

這個函數要回答:

按照當前排序規則,a 是否應該排在 b 前面?

例如:

rule("Bob", "Charlie");

因爲:

Bob      长度 3
Charlie  长度 7

最終判斷的是:

3 < 7

結果爲 true,表示按照當前規則,Bob 應該排在 Charlie 前面。

如果兩個字符串長度相同:

if (a.size() == b.size())
{
    return a < b;
}

就再使用字符串的字典順序決定誰在前面。例如 AliceDavid 長度都是 5,因此比較:

"Alice" < "David"

結果爲 true,所以 Alice 排在 David 前面。

三個算法可以這樣記:

API第三個參數回答什麼?
count_if這個元素算不算?
find_if這個元素是不是我要找的?
sorta 應不應該排在 b 前面?

這正是 Lambda 很常見的用途:STL 算法負責通用流程,Lambda 負責描述具體規則。

const std::string&.size()

Lambda 中的:

const std::string& a

可以拆成:

  • std::string:參數是字符串;
  • &:使用引用,不額外複製整個字符串;
  • const:只讀取這個字符串,不修改它。

因此 const std::string& 常用於只讀地接收 stringvector 等較大的對象。

字符串的:

a.size()

返回字符串長度。例如:

"Bob"       -> 3
"Alice"     -> 5
"Charlie"   -> 7
"David"     -> 5

所以示例的排序結果是:

Bob Alice David Charlie

範圍 for

for (const auto& name : names)
{
    std::cout << name << " ";
}

可以直接理解爲:

依次取出 names 中的每個元素,並把當前元素命名爲 name

這裏的:

const auto& name

編譯器會根據 names 的元素類型自動推導,近似等價於:

const std::string& name

static_cast<std::size_t>

這裏:

static_cast<std::size_t>(min_length)

是顯式類型轉換,可以簡單理解爲:

min_length 轉換成 std::size_t 類型。

因爲 name.size() 返回的是 std::size_t,而:

int min_length = 6;

中的 min_lengthint。轉換以後,兩邊使用適合的同類整數類型進行比較。

std::size_t 常用於表示容器大小、字符串長度和數組下標等非負數量。

it 和迭代器

auto it = std::find_if(...);

find_if 返回的不是字符串本身,而是一個迭代器

迭代器可以暫時理解成類似指針的對象,用來表示容器中的某個位置。

排序後的數據是:

Bob    Alice    David    Charlie
                         ↑
                         it

因此:

*it

取得的就是:

Charlie

如果 find_if 一直沒有找到滿足條件的元素,它會返回:

names.end()

所以常見寫法是:

if (it != names.end())
{
    std::cout << *it << "\n";
}

意思就是:

如果確實找到了元素,再使用它。

整段代碼的執行過程

程序首先創建:

std::vector<std::string> names = {
    "Bob",
    "Alice",
    "Charlie",
    "David"
};

然後 std::sort 使用 Lambda 作爲排序規則:

  1. 長度不同,短的排前面。
  2. 長度相同,按照字符串字典順序排列。

所以得到:

Bob Alice David Charlie

接下來:

int min_length = 6;

規定要尋找長度至少爲 6 的名字。

std::find_if 會按照當前順序依次調用 Lambda:

Bob       -> 3 >= 6 -> false
Alice     -> 5 >= 6 -> false
David     -> 5 >= 6 -> false
Charlie   -> 7 >= 6 -> true

第一次得到 true 時停止,因此 it 最終指向 Charlie

*it

得到:

Charlie

最終輸出:

sort by length: Bob Alice David Charlie
first long name = Charlie

這個示例體現了 Lambda 非常典型的一種用途:

STL 算法負責通用流程,Lambda 負責描述具體規則。

示例 6:保存回調時要注意捕獲生命週期

如果 lambda 只是立刻調用,引用捕獲通常看起來沒問題;如果保存到容器、線程、定時器或異步回調裏,lambda 可能晚於局部變量執行。保存回調時優先按值捕獲需要的數據。

#include <functional>
#include <iostream>
#include <string>
#include <vector>

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // std::function 可以保存普通函数、lambda 或函数对象。
    // vector 是动态数组,元素数量可以在运行时变化。
    std::vector<std::function<void()>> callbacks;

    {
        std::string name = "Alice";
        int score = 95;

        auto print_now = [&name, &score]() {
            std::cout << "now: " << name << " " << score << "\n";
        };
        print_now();

        callbacks.push_back([name, score]() {
            std::cout << "saved: " << name << " " << score << "\n";
        });
    }

    for (const auto& callback : callbacks)
    {
        callback();
    }

    return 0;
}

運行結果

now: Alice 95
saved: Alice 95

這裏 print_now 立刻調用,所以引用捕獲沒問題。保存到 callbacks 的 lambda 用按值捕獲,因爲離開內部作用域後,namescore 已經銷燬。

示例 7:捕獲 this 訪問當前對象

在類的成員函數中,Lambda 可以使用 [this] 捕獲當前對象的指針。Lambda 訪問的仍然是原來的對象,因此對象成員之後發生的變化也能看到。

#include <iostream>

class Robot
{
public:
    Robot(int speed) : speed_(speed)
    {
    }

    void demo()
    {
        auto print_speed = [this]() {
            std::cout << "lambda speed = " << speed_ << "\n";
        };

        speed_ = 20;
        print_speed();
    }

private:
    int speed_;
};

int main()
{
    Robot robot(10);
    robot.demo();

    return 0;
}

運行結果

lambda speed = 20

這裏:

[this]() {
    std::cout << speed_;
}

可以近似理解爲 Lambda 保存了當前對象的 this 指針,因此:

speed_

實際上訪問的是:

this->speed_

創建 Lambda 時 speed_10,之後原對象把 speed_ 修改成 20。因爲 [this] 指向的仍然是原對象,所以調用 Lambda 時讀到的是 20

需要特別注意:[this] 保存的是對象指針,不是對象副本。如果 Lambda 被保存下來,而原對象已經銷燬,再通過這個 Lambda 訪問成員就可能產生懸空指針問題。因此保存回調、異步任務和線程回調中使用 [this] 時要特別關注對象生命週期。

示例 8:使用 [*this] 捕獲當前對象副本

C++17 開始可以使用 [*this]。它不是保存當前對象的指針,而是在創建 Lambda 時複製一份當前對象。

#include <iostream>

class Robot
{
public:
    Robot(int speed) : speed_(speed)
    {
    }

    void demo()
    {
        auto print_speed = [*this]() {
            std::cout << "lambda copy speed = " << speed_ << "\n";
        };

        speed_ = 20;

        print_speed();
        std::cout << "outside speed = " << speed_ << "\n";
    }

private:
    int speed_;
};

int main()
{
    Robot robot(10);
    robot.demo();

    return 0;
}

運行結果

lambda copy speed = 10
outside speed = 20

這裏 Lambda 創建時:

[*this]

會把當時的 Robot 對象複製一份保存到 Lambda 內部。此時副本中的:

speed_ = 10

之後:

speed_ = 20;

修改的是外面的原對象,不會修改 Lambda 已經保存的對象副本,因此 Lambda 仍然輸出 10

可以把兩種寫法這樣對比:

寫法Lambda 保存什麼原對象後來修改成員後
[this]當前對象的指針Lambda 能看到新的成員值
[*this]當前對象的副本Lambda 保留捕獲時的成員值

[*this] 可以避免因爲原對象銷燬而直接留下一個懸空的 this 指針,但它會複製整個對象,因此也要考慮對象是否適合複製以及複製成本。

如果需要在 Lambda 中修改這份對象副本,與普通按值捕獲類似,可以再配合 mutable

auto callback = [*this]() mutable {
    ++speed_;
};

這裏修改的仍然只是 Lambda 內部保存的對象副本,不會修改外面的原對象。

關鍵語法解釋

示例重點說明
示例 1舊回調寫法對比普通函數、函數對象都能做回調,但 lambda 更適合局部短邏輯
示例 2參數和返回值返回類型通常可推導,分支返回不同類型時要顯式寫清楚
示例 3捕獲列表[x] 拷貝,[&x] 引用
示例 4mutable修改的是 lambda 內部副本,不影響外部變量
示例 5STL 算法sortfind_ifcount_if 常和 lambda 配合
示例 6生命週期保存回調、異步回調、線程回調中不要隨便引用捕獲局部變量
示例 7[this]捕獲當前對象指針,Lambda 訪問的是原對象
示例 8[*this]C++17 起捕獲當前對象副本,與原對象狀態分離

常見錯誤

  1. 保存回調時使用默認引用捕獲 [&],導致局部變量銷燬後仍被訪問。
  2. 以爲按值捕獲會跟着外部變量變化。按值捕獲保存的是定義 lambda 時的副本。
  3. 想修改按值捕獲的副本,卻忘記加 mutable
  4. 把有捕獲的 lambda 當成函數指針使用。有捕獲的 lambda 需要用模板參數、auto 變量或 std::function 保存。
  5. 異步場景捕獲 this 後,對象先被銷燬。需要保證對象生命週期,或使用智能指針、[*this] 等更明確的方式。

使用建議

  1. 小而局部的邏輯優先用 lambda。
  2. 捕獲列表儘量顯式寫 [x, &y],少用默認 [=][&]
  3. 保存回調、線程、定時器、異步操作中優先按值捕獲需要的數據。
  4. 只調用一次、不需要保存的回調,可以直接把 lambda 傳給算法或函數模板。
  5. 類成員函數中的回調使用 [this] 時要保證對象生命週期;需要獨立保存對象狀態時可以考慮 C++17 的 [*this]
  6. 需要統一保存不同 lambda 時,再使用下一節的 std::function

小結

  • Lambda 是匿名可調用對象,寫法是 [捕获](参数) { 函数体 }
  • 捕獲列表決定 lambda 如何使用外部變量。
  • [x] 是按值捕獲,[&x] 是按引用捕獲。
  • mutable 允許修改按值捕獲的內部副本。
  • [this] 捕獲當前對象指針,[*this] 從 C++17 起可以捕獲當前對象副本。
  • Lambda 最常用於 STL 算法、回調和異步任務。
  • 生命週期是 Lambda 最容易出錯的地方,尤其是保存回調和異步回調。
音乐页