2. C++11 中的 Lambda


萬歲!C++ 委員會聽取了開發(fā)人員的意見,在 C++11 標(biāo)準(zhǔn)中加入了 lambda 表達(dá)式!

Lambda 表達(dá)式很快就成為現(xiàn)代 C++ 中最具辨識度的一個(gè)特性。

你可以在 N3337(C++11 的最終草案)中閱讀其完整規(guī)范,以及關(guān)于 lambda 的單獨(dú)部分:[express .prim.lambda]。

我認(rèn)為委員會以一種聰明的方式在語言中添加了 lambda。他們設(shè)計(jì)了新的語法,但隨后編譯器將其“展開”為一個(gè)未命名的“隱藏的”函數(shù)對象類型。這樣我們就擁有了真正強(qiáng)類型語言的所有優(yōu)點(diǎn)(以及缺點(diǎn)),使代碼理解起來更加容易。

在本章,你將會學(xué)習(xí)到:

  • Lambda 的基礎(chǔ)語法。
  • 如何捕獲一個(gè)變量。
  • 如何捕獲一個(gè)類的非靜態(tài)成員變量。
  • Lambda 的返回類型。
  • 什么是閉包類型。
  • 怎樣將 lambda 表達(dá)式轉(zhuǎn)換成一個(gè)函數(shù)指針從而能夠去使用 C 風(fēng)格的 API.
  • 什么是 IIFE 以及為什么它是的有用的。
  • 如何繼承一個(gè) lambda 表達(dá)式。

讓我們出發(fā)吧!


Lambda 表達(dá)式的語法

下圖說明了 C++11 中 lambda 的語法:

現(xiàn)在讓我們通過幾個(gè)例子來感受一下它。


Lambda 表達(dá)式的幾個(gè)例子

// 1. 最簡單的 lambda 表達(dá)式:
[] {};

在第一個(gè)示例中,你可以看到一個(gè)“最迷你”的 lambda 表達(dá)式。它只需要[]部分
(lambda 引入器),然后用空的{}部分作為函數(shù)體。形參列表()是可選的,在本例中不需要。

// 2. 擁有兩個(gè)參數(shù)的 lambda:
[] (float f, int a) { return a * f; };
[] (int a, int b) { return a < b; };

在第二個(gè)例子中,可能是最常見的例子了,你可以看到參數(shù)都傳遞到()部分,就像普通函數(shù)一樣。返回類型不需要,因?yàn)榫幾g器會自動(dòng)推導(dǎo)它。

// 3. 尾置返回類型:
[] (MyClass t) -> int { auto a = t.compute(); print(a); return a; };

在上面的例子中,我們顯式地設(shè)置了一個(gè)返回類型。后面的返回類型也可用在 C++11 以來的常規(guī)函數(shù)聲明中。

// 4. 額外的說明符:
[x] (int a, int b) mutable { ++x; return a < b; };
[] (float param) noexcept { return param * param; };
[x] (int a, int b) mutable noexcept { ++x; return a < b; };

最后一個(gè)示例顯示,在 lambda 的主體之前,可以使用其他說明符。在代碼中,我們使用了 mutable(這樣我們可以改變捕獲的變量)和noexcept。第三個(gè) lambda 使用了mutablenoexcept,并且它們必須以該順序出現(xiàn)(你不能寫noexcept mutable,因?yàn)榫幾g器會拒絕它)。

雖然()部分是可選的,但如果你想應(yīng)用 mutablenoexcept,此時(shí)()則需要在出現(xiàn)的表達(dá)中:

// 5. 可選項(xiàng)
[x] { std::cout << x; }; // 不需要 () 
[x] mutable { ++x; }; // 無法通過編譯!
[x] () mutable { ++x; }; // 可以,mutable 前面的 () 是必要的
[] noexcept { }; // 無法通過編譯!
[] () noexcept { }; // 可以

同樣的模式也適用于其他可以應(yīng)用于 lambdas 的說明符,比如 C++17 中的 constexpr 和 C++20 中的 consteval

在熟悉了基本的例子之后,我們現(xiàn)在可以嘗試去理解它是如何工作的,并學(xué)習(xí) lambda 表達(dá)式的所有可能用法。


核心定義

在我們繼續(xù)之前,從 C++ 標(biāo)準(zhǔn)中引入一些核心定義是很方便的:
來自 [expr.prim.lambda#2]

lambda 表達(dá)式的計(jì)算結(jié)果是一個(gè)臨時(shí)的純右值。這個(gè)臨時(shí)值叫做閉包對象。

作為旁注,lambda 表達(dá)式是一個(gè) prvalue 即“純右值” 。這種類型的表達(dá)式通常產(chǎn)生自初始化并出現(xiàn)在賦值的右側(cè)(或在 return 語句中)。閱讀 C++ Reference,[express .prim.lambda#3] 中給出的的另一個(gè)定義是:

lambda 表達(dá)式的類型(也就是閉包對象的類型)是一個(gè)唯一的,未命名的非聯(lián)合類類型——稱為閉包類型。


編譯器展開

從以上定義中,我們可以了解到編譯器從一個(gè) lambda 表達(dá)式生成唯一的閉包類型。然后我們可以通過這個(gè)類型來實(shí)例化出閉包對象。

以下示例展示了如何寫一個(gè) lambda 表達(dá)式并將其傳給std::for_each。為了便于比較,代碼還說明了編譯器生成的相應(yīng)的函數(shù)對象類型:

// Ex2_1: Lambda 和 相應(yīng)的函數(shù)對象。
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    struct {
        void operator()(int x) const {
            std::cout << x << '\n';
        }
    } someInstance;

    const std::vector<int> v { 1, 2, 3 };
    std::for_each(v.cbegin(), v.cend(), someInstance);
    std::for_each(v.cbegin(), v.cend(), [] (int x) {
            std::cout << x << '\n';
        }
    );
}

在本例中,編譯器將
[](int x) { std::cout << x << '\n'; }
翻譯成一個(gè)匿名函數(shù)對象,簡化形式如下:

struct {
    void operator()(int x) const {
        std::cout << x << '\n';
    }
} someInstance;

“翻譯”或“展開”的過程可以很容易地在 C++ Insights 在線網(wǎng)頁工具上看到。該工具獲取有效的 C++ 代碼,然后產(chǎn)生編譯器需要的源代碼版本:像 lambda 的匿名函數(shù)對象,模板的實(shí)例化等其他 C++ 的特性。

在下一節(jié)中,我們將深入研究 lambda 表達(dá)式的各個(gè)部分。


Lambda 表達(dá)式的類型

由于編譯器為每個(gè) lambda (閉包類型)生成唯一的名稱,所以我們就沒法把它“拼寫”在前面。

這就是為什么必須使用auto(或 decltype)來推斷其類型。
auto myLambda = [](int a) -> double { return 2.0 * a; };
而且,如果你有兩個(gè)看起來一樣的 lambda:
auto firstLam = [](int x) { return x * 2; };
auto secondLam = [](int x) { return x * 2; };
它們的類型也是不同的,即使“代碼背后”是相同的!編譯器需要為這兩個(gè) lambda 聲明的每個(gè)都生成惟一的匿名類型。我們可以用下面的代碼來證明這個(gè)屬性:

// Ex2_1: 相同的代碼,不同的類型。
#include <type_traits>

int main() {
    const auto oneLam = [](int x) noexcept { return x * 2; };
    const auto twoLam = [](int x) noexcept { return x * 2; };
    static_assert(!std::is_same<decltype(oneLam), decltype(twoLam)>::value,
                  "must be different!");
}

上面的例子驗(yàn)證了 oneLam 和 twoLam 的閉包類型是否不相同。

在 C++17 中我們可以使用無需消息的static_assert以及用于類型萃取的輔助變量模板is_same_vstatic_assert(std::is_same_v<double, decltype(baz(10))>);

然而,雖然你不知道確切的名稱,但是你還是可以拼出 lambda 的簽名,然后將其存儲在std::function中。一般來說,如果 lambda 是通過 std::function<>類型“表示”的,那么它可以完成定義為auto的 lambda 無法完成的任務(wù)。例如,前面的 lambda 具有double(int)的簽名,因?yàn)樗邮?code>int作為輸入?yún)?shù)并返回double。然后我們可以用以下方法創(chuàng)建std::function對象:

std::function<double(int)> myFunc = [](int a) -> double { return 2.0 * a; };

std::function 是一個(gè)重量級的對象,因?yàn)樗枰幚硭锌烧{(diào)用對象。要做到這一點(diǎn),它需要高級的內(nèi)部機(jī)制,如類型雙關(guān)語,甚至是動(dòng)態(tài)內(nèi)存分配。我們可以通過一個(gè)簡單的實(shí)驗(yàn)來檢驗(yàn)它的大小:

// Ex2_3: std::function 和 auto 類型推導(dǎo)。
#include <functional>
#include <iostream>

int main() {
    const auto myLambda = [](int a) noexcept -> double {
        return 2.0 * a;
    };

    const std::function<double(int)> myFunc =
        [](int a) noexcept -> double {
        return 2.0 * a;
    };

    std::cout << "sizeof(myLambda) is " << sizeof(myLambda) << '\n';
    std::cout << "sizeof(myFunc) is " << sizeof(myFunc) << '\n';

    return myLambda(10) == myFunc(10);
}

在 GCC 編譯下代碼輸出如下:
sizeof(myLambda) is 1
sizeof(myFunc) is 32
因?yàn)?myLambda 只是一個(gè)無狀態(tài)的 lambda,所以它也是一個(gè)空類,沒有任何數(shù)據(jù)成員字段,所以它的最小大小只有一個(gè)字節(jié)。另一邊的std::function版本則要大得多——32 個(gè)字節(jié)。這就是為什么如果可以的話,應(yīng)該依靠自動(dòng)類型推導(dǎo)來獲得盡可能小的閉包對象。

當(dāng)我們討論std::function時(shí),還需要注意的是,這種類型不是只移型閉包。你可以在 C++14 的可移動(dòng)的類型章節(jié)中閱讀關(guān)于這個(gè)問題的更多信息。

構(gòu)造和復(fù)制

在特性規(guī)范 [expr.prim.lambda] 里我們 可以讀到如下信息:

一個(gè) lambda 表達(dá)式關(guān)聯(lián)的閉包類型擁有一個(gè)刪除的默認(rèn)構(gòu)造函數(shù)和一個(gè)刪除的復(fù)制賦值運(yùn)算符。

這就是為什么你無法寫出:
auto foo = [&x, &y]() { ++x; ++y; };
decltype(foo) fooCopy;
在 GCC 上這段代碼將出現(xiàn)如下錯(cuò)誤提示:


然而,你可以復(fù)制 lambda:

// Ex2_4: Copying lambdas. Live code
#include <type_traits>

int main() {
    const auto firstLam = [](int x) noexcept { return x * 2; };
    const auto secondLam = firstLam;
    static_assert(std::is_same<decltype(firstLam), decltype(secondLam)>::value,
                  "must be the same");
}

如果你復(fù)制一個(gè) lambda,那么你也復(fù)制了它的狀態(tài)。當(dāng)我們討論捕獲變量時(shí),這一點(diǎn)很重要。在該上下文中,閉包類型將捕獲的變量存儲為成員字段。執(zhí)行 lambda 復(fù)制將復(fù)制這些數(shù)據(jù)成員字段。

展望未來
在 C++20 中,無狀態(tài) lambda 將是默認(rèn)為可構(gòu)造和可賦值的。

調(diào)用運(yùn)算符

放入 lambda 體中的代碼被“翻譯”為對應(yīng)閉包類型的operator()中的代碼。

在 C++11 中,默認(rèn)情況下它是一個(gè)const inline成員函數(shù)。例如:
auto lam = [](double param) { /* do something*/ };
將會被展開成類似于:

struct __anonymousLambda {
    inline void operator()(double param) const { /* do something */ }
};

接下來我們討論這種方法的結(jié)果,以及如何修改生成的調(diào)用操作符聲明。

重載

值得一提的是,在定義 lambda 時(shí),無法創(chuàng)建接受不同參數(shù)的“重載” lambda。如:
// doesn't compile!
auto lam = [](double param) { /* do something*/ };
auto lam = [](int param) { /* do something*/ };
以上代碼無法通過編譯,由于編譯器無法將這兩個(gè) lambda 翻譯到一個(gè)函數(shù)對象。此外,你不能定義兩個(gè)相同的變量。然而,創(chuàng)建同一個(gè)函數(shù)對象中的兩個(gè)調(diào)用運(yùn)算符的重載卻是允許的:

struct MyFunctionObject {
    inline void operator()(double param) const { /* do something */ }
    inline void operator()(int param) const { /* do something */ }
};

MyFunctionObject可以同時(shí)接收doubleint這兩種參數(shù)。如果你需要類似行為的 lambda,你可以去看關(guān)于 lambda 繼承的小節(jié)或是 C++17 中的重載模式小節(jié)。

屬性

C++11 允許以[[attr_name]]的語法去給 lambda 設(shè)置屬性。但是,如果將一個(gè)屬性應(yīng)用到 lambda,那么它將應(yīng)用于調(diào)用的類型而不是運(yùn)算符本身。這就是為什么現(xiàn)在(甚至在c++ 20中)有沒有對 lambda 有意義的屬性。大多數(shù)編譯器甚至?xí)?bào)告錯(cuò)誤。如果我們?nèi)∫粋€(gè) C++17 的屬性,并嘗試將它與表達(dá)式一起使用:
auto myLambda = [](int a) [[nodiscard]] { return a * a; };
這會在 Clang 上生成以下錯(cuò)誤:
error: 'nodiscard' attribute cannot be applied to types
雖然理論上已經(jīng)準(zhǔn)備好了 lambda 語法,但目前還沒有適用的語法屬性。

其他變化

我們在語法部分簡要介紹了這個(gè)主題,但是你并不局限于閉包類型的調(diào)用操作符的默認(rèn)聲明。在 C++11 中,您可以添加 mutable 或異常聲明符。

如果可能的話,本書中較長的例子嘗試用 const 標(biāo)記閉包對象,并使 lambda noexcept。

你可以通過在參數(shù)聲明子句后面指定 mutable 和 noexcept 來使用這些關(guān)鍵字:
auto myLambda = [](int a) mutable noexcept { /* do something */ }
編譯器將生成如下代碼:

struct __anonymousLambda {
    inline void operator()(double param) noexcept { /* do something */ }
};

請注意,const 關(guān)鍵字現(xiàn)在沒有了,調(diào)用操作符現(xiàn)在可以更改 lambda 的數(shù)據(jù)成員。
但是什么數(shù)據(jù)成員呢?如何聲明 lambda 的數(shù)據(jù)成員?請參閱下一節(jié)關(guān)于變量的“捕獲”:

捕獲

方括號 [] 不僅引導(dǎo)了 lambda而且還保存了捕獲變量的列表。因此,它也被稱作“捕獲子句”。

通過從 lambda 外部作用域捕獲一個(gè)變量, 你為閉包類型創(chuàng)建了一個(gè)非靜態(tài)數(shù)據(jù)成員。進(jìn)而在 lambda 主體內(nèi)部,你可以訪問到它。

在 C++98/03 章節(jié),我們?yōu)?Printer 函數(shù)對象做了相似的事情。在那個(gè)類里,我添加了一個(gè) std::string 類型的數(shù)據(jù)成員 strText 并在構(gòu)造函數(shù)中做了初始化??烧{(diào)用對象擁有一個(gè)數(shù)據(jù)成員使我們能夠保存它的狀態(tài)。

C++11 中的捕獲語法是這樣的:

語法 描述
[&] 通過引用捕獲在到達(dá)作用域中聲明的所有自動(dòng)存儲持續(xù)時(shí)間變量
[=] 按值捕獲(創(chuàng)建副本)在到達(dá)范圍中聲明的所有自動(dòng)存儲持續(xù)時(shí)間變量
[x, &y] 通過值顯式捕獲x,通過引用顯式捕獲y
[args...] 按值捕獲模板參數(shù)包
[&args...] 按引用捕獲模板參數(shù)包
this 捕獲成員函數(shù)內(nèi)部的this指針

請注意,對于 [=] 和 [&] 情況,編譯器會為 lambda 主體內(nèi)所有使用的變量生成數(shù)據(jù)成員。這是一種方便的語法,您不需要顯式提及捕獲的變量。

下面是基本語法的總結(jié)和示例:

int x = 2, y = 3;
const auto l1 = []() { return 1; }; // 無捕獲
const auto l2 = [=]() { return x; }; // lambda 中使用的所有變量都會被復(fù)制
const auto l3 = [&]() { return y; }; // lambda 中使用的所有變量都會被引用
const auto l4 = [x]() { return x; }; // 僅通過值捕獲 x (復(fù)制)
// const auto lx = [=x]() { return x; }; // 語法錯(cuò)誤,顯示復(fù)制 x 不需要 = 
const auto l5 = [&y]() { return y; }; // 僅通過引用捕獲 y
const auto l6 = [x, &y]() { return x * y; }; // x 值捕獲 y 引用捕獲
const auto l7 = [=, &x]() { return x + y; }; // x 以引用捕獲,除此之外都以值捕獲
const auto l8 = [&, y]() { return x - y; }; // y 以值捕獲,除此之外都以引用捕獲

什么是“自動(dòng)存儲期”?
程序中的所有對象都有四種可能的“存儲”方式:automatic(自動(dòng)存儲)、static(靜態(tài))、thread(線程)或 dynamic(動(dòng)態(tài))。自動(dòng)意味著在作用域開始時(shí)分配存儲,就像在函數(shù)中一樣。大多數(shù)局部變量都有自動(dòng)存儲期(聲明為 static、extern 或 thread_local 的除外)。詳見 cppreference - storage duration。

為了理解捕獲一個(gè)變量時(shí)到底發(fā)生了什么,讓我們來考慮以下代碼:

std::string str {"Hello World"};
auto foo = [str]() { std::cout << str << '\n'; };
foo();

對于上面的 lambda,str 是按值進(jìn)行捕獲的(也就是被復(fù)制)。編譯器可能會為此生成如下的局部函數(shù)對象:

struct _unnamedLambda {
  inline void operator()() const {
    std::cout << str << '\n';
  }
  std::string str;
};

當(dāng)你將一個(gè)變量傳遞給捕獲子句時(shí),它就被用來直接初始化數(shù)據(jù)成員 str。所以前面的例子可以“展開”為:

std::string str {"Hello World"};
_unnamedLambda foo { str };
foo();

標(biāo)準(zhǔn)詳見 [expr.prim.lambda#21]

當(dāng)計(jì)算lambda表達(dá)式時(shí),使用值捕獲的實(shí)體直接初始化結(jié)果閉包對象的每個(gè)相應(yīng)的非靜態(tài)數(shù)據(jù)成員。

再來看一個(gè)捕獲了兩個(gè)變量的例子:

int x = 1, y = 1;
std::cout << x << " " << y << '\n';
const auto foo = [&x, &y] noexcept { ++x; ++y; };
foo();
std::cout << x << " " << y << '\n';

對于以上 lambda,編譯器可能生成如下局部函數(shù)對象:

struct _unnameLambda {
  void operator()() const noexcept {
    ++x; ++y;
  };
  int& x;
  int& y;
};

由于我們按引用捕獲了 x 和 y;閉包類型將會包含兩個(gè)數(shù)據(jù)成員,而且都是引用。

值捕獲變量的值是在定義 lambda 時(shí)的值,而不是在調(diào)用時(shí)的值!引用捕獲的變量的值是使用 lambda 時(shí)的值,而不是定義它時(shí)的值。

C++ 閉包不會延長捕獲的引用的生存期。確保在調(diào)用 lambda 時(shí)捕獲變量仍然存在。

代碼生成

在本書中,我展示了一個(gè)可能的編譯器生成的代碼,作為一個(gè)結(jié)構(gòu)體來定義閉包類類型。然而,這只是一種簡化——一種理想模型——在編譯器內(nèi)部,情況可能會有所不同。

例如,對于 Clang,它的抽象語法生成樹(AST:Abstract Syntax Tree)就使用類來表示一個(gè)閉包。其調(diào)用運(yùn)算符被定義為共有的,而數(shù)據(jù)成員則被定義為私有的。

這就是為什么我們無法寫出這樣的代碼:

int x = 0;
auto lam = [=]() { std::cout << x; };
lam.x = 10; // ?? 

在 GCC (在 Clang 與之類似)將會得到如下報(bào)錯(cuò)信息:
error: 'struct main()::<lambda()>' has no member named 'x'

另一方面,規(guī)范的一個(gè)重要部分提到,捕獲的變量是直接初始化的,這對于私有成員(對于代碼中的常規(guī)類)是不可能的。這意味著編譯器可以在這里發(fā)揮一點(diǎn)“魔力”,創(chuàng)建更高效的代碼(不需要復(fù)制變量,甚至不需要移動(dòng)它們)。

如果你想了解更多關(guān)于 Lambda 的內(nèi)部實(shí)現(xiàn)細(xì)節(jié),請移步至 Andreas Fertig(C++ Insights 的創(chuàng)辦人) 的博客:Under the covers of C++ lambdas - Part 2: Captures, captures, captures。

捕獲所有或顯式捕獲

雖然指定 [=] 或 [&] 可能很方便,因?yàn)樗东@了所有自動(dòng)存存儲期的變量,然而顯式捕獲一個(gè)變量會更清晰。這樣,編譯器就可以警告你不想要的效果(例如,請參閱關(guān)于全局變量和靜態(tài)變量的說明)。

你也可以在 Scott Meyer 著的 《Effective Modern C++》的條款 31:“避免默認(rèn)捕獲模式”了解更多相關(guān)信息。

關(guān)鍵字 mutable

閉包類型的 operator() 默認(rèn)被標(biāo)記為 const,因此你無法在 lambda 體內(nèi)改變捕獲到的變量值。

如果你想改變這個(gè)行為,你需要在形參列表后面添加 mutable 關(guān)鍵字。這種語法有效地從閉包類型的調(diào)用操作符聲明中刪除了const。如果你有定義了一個(gè)帶有 mutable 關(guān)鍵字的 lambda 表達(dá)式:

int x = 1;
auto foo = [x]() mutable { ++x; };

它將會被“拓展”成如下函數(shù)對象:

struct __lambda_x1 {
  void operator()() { ++x; }
  int x;
};

如你所見,調(diào)用運(yùn)算符重載可以更改成員字段的值了。

// Ex2_5: Capturing Two Variables by Copy and Mutable.
#include <iostream>

int main() {
  const auto print = [](const char* str, int x, int y) {
    std::cout << str << ": " << x << " " << y << '\n';
  };
  int x = 1, y = 1;
  print("in main()", x, y);
  auto foo = [x, y, &print]() mutable {
    ++x;
    ++y;
    print("in foo()", x, y);
  };
  foo();
  print("in main()", x, y);
}

// 輸出:
// in main(): 1 1
// in foo(): 2 2
// in main(): 1 1

在上例中,我們可以改變 x 和 y 的值。因?yàn)樗鼈冎皇欠忾]作用域中 x 和 y 的副本,所以在調(diào)用 foo 之后我們看不到它們的新值。

另一方面,如果通過引用捕獲,則不需要對 lambda 應(yīng)用mutable 修改該值。這是因?yàn)椴东@的數(shù)據(jù)成員是引用,這意味著無論如何都不能將它們綁定到新對象,但可以更改引用的值。

int x = 1;
std::cout << x << '\n';
const auto foo = [&x]() noexcept { ++x; };
foo();
std::cout << x << '\n';

在上面的例子中,lambda 沒有被指定為 mutable,但是它可以改變被引用的值。

需要注意的一件重要事情是,當(dāng)應(yīng)用 mutable 時(shí),不能用const 標(biāo)記生成的閉包對象,因?yàn)檫@會阻止對 lambda 的調(diào)用!

int x = 10;
const auto lam = [x]() mutable { ++x; }
lam(); // 無法編譯!

最后一行不能編譯,因?yàn)椴荒茉?const 對象上調(diào)用非 const成員函數(shù)。

調(diào)用計(jì)數(shù)器——捕獲變量的一個(gè)例子

在我們開始討論關(guān)于捕獲的更復(fù)雜的主體之前,我們可以休息一下,來專注于一個(gè)更加實(shí)際的例子。

當(dāng)你想要使用標(biāo)準(zhǔn)庫中的某些現(xiàn)有算法并更改其默認(rèn)行為時(shí),Lambda表達(dá)式非常方便。例如,對于 std::sort 你可以傳入你自己的比較函數(shù)。

但是我們可以更進(jìn)一步,傳入一個(gè)有調(diào)用計(jì)數(shù)器的增強(qiáng)版比較函數(shù)。

// Ex2_6: Invocation Counter.
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
  std::vector<int> vec { 0, 5, 2, 9, 7, 6, 1, 3, 4, 8 };
  size_t compCounter = 0;
  std::sort(vec.begin(), vec.end(),
    [&compCounter](int a, int b) noexcept {
      ++compCounter;
    return a < b;
    }
  );
  std::cout << "number of comparisons: " << compCounter   << '\n';
  for (const auto& v : vec)
    std::cout << v << ", ";
}

示例中提供的比較器的工作方式與默認(rèn)比較器相同,如果 a 小于 b,它將返回,因此我們使用從小到大的自然順序。然而,傳遞給 std::sort 的 lambda 也捕獲了一個(gè)局部變量 compCounter。然后使用該變量對排序算法中對該比較器的所有調(diào)用進(jìn)行計(jì)數(shù)。

捕獲全局變量

如果你試圖在你的 lambda 中使用 [=] 來捕獲一個(gè)全局變量 ,你可能認(rèn)為這個(gè)全局對象也會以傳值的方式被捕獲......,但并非如此??创a:

// Ex2_7: Capturing Globals.
#include <iostream>

int global = 10;

int main() {
  std::cout << global << '\n';
  auto foo = [=]() mutable noexcept { ++global; };
  foo();
  std::cout << global << '\n';
  const auto increaseGlobal = []() noexcept { ++global; };
  increaseGlobal();
  std::cout << global << '\n';
  const auto moreIncreaseGlobal = [global]() noexcept {     ++global; };
  moreIncreaseGlobal();
  std::cout << global << '\n';
}

上例中定義了一個(gè)全局變量,然后在 main 函數(shù)中定義的幾個(gè) lambda 中使用它。如果你運(yùn)行這段代碼,那么無論你以何種方式捕獲,它都將始終指向全局對象,并不會創(chuàng)建本地副本。

這是因?yàn)橹挥芯哂凶詣?dòng)存儲期的變量才能被捕獲。GCC 甚至可以報(bào)出以下警告:
warning: capture of variable 'global' with non-automatic storage duration
只有在顯式捕獲全局變量時(shí)才會出現(xiàn)此警告,因此如果使用 [=],編譯器也幫不了你。

Clang 編譯器甚至更有幫助,因?yàn)樗鼤梢粋€(gè)錯(cuò)誤:
error: 'global' cannot be captured because it does not have automatic storage duration

捕獲靜態(tài)變量

與捕獲全局變量類似,對于靜態(tài)對象,你也會得到相似的錯(cuò)誤:

// Ex2_8: Capturing Static Variables. 
#include <iostream>

void bar() {
  static int static_int = 10;
  std::cout << static_int << '\n';
  auto foo = [=]() mutable noexcept{ ++static_int; };
  foo();
  std::cout << static_int << '\n';
  const auto increase = []() noexcept { ++static_int; };
  increase();
  std::cout << static_int << '\n';
  const auto moreIncrease = [static_int]() noexcept {     ++static_int; };
  moreIncrease();
  std::cout << static_int << '\n';
}

int main() {
  bar();
}

// 輸出
// 10
// 11
// 12
// 13

這次,我們嘗試捕獲一個(gè)靜態(tài)變量,然后更改它的值,但由于它不是自動(dòng)存儲期,編譯器無法做到這一點(diǎn)。

當(dāng)你通過名稱 [static_int] 捕獲變量時(shí),GCC 報(bào)告一個(gè)警告,而 Clang 顯示一個(gè)錯(cuò)誤。

捕獲類成員和 this 指針

當(dāng)你在類成員函數(shù)中,并且希望捕獲數(shù)據(jù)成員時(shí),事情會變得稍微復(fù)雜一些。由于所有非靜態(tài)數(shù)據(jù)成員都與 this 指針相關(guān),因此它也必須存儲在某個(gè)地方。

看代碼:

// Ex2_9: Error when capturing a data member. 
#include <iostream>
struct Baz {
  void foo() {
    const auto lam = [s]() { std::cout << s; };
    lam();
  }
  std::string s;
};

int main() {
  Baz b;
  b.foo();
}

這段代碼嘗試去捕獲一個(gè)數(shù)據(jù)成員 s。然而編譯器卻發(fā)出了如下錯(cuò)誤信息:

In member function 'void Baz::foo()':
error: capture of non-variable 'Baz::s'
error: 'this' was not captured for this lambda function

為了解決這個(gè)錯(cuò)誤,我們必須捕獲 this 指針。之后我們就能訪問到數(shù)據(jù)成員了。

我們將代碼更新為:

struct Baz {
  void foo() {
    const auto lam = [this]() { std::cout << s; };
    lam();
  }
  std::string s;
};

現(xiàn)在就沒有編譯錯(cuò)誤了。

你也可以使用 [=] 或 [&] 去捕獲 this 指針(它們在 C++ 11/14 中具有相同的效果)。

請注意,我們通過指針的值捕獲了 this。這就是為什么您可以訪問初始數(shù)據(jù)成員,而不是它的副本。

在 C++11(甚至是 C++14)中你無法這樣寫:
auto lam = [*this]() { std::cout << s; }
這段代碼在 C++11/14 下無法編譯,然而,在 C++17 下可以。

如果您在單個(gè)方法的上下文中使用 lambda,那么捕獲 this 將很好。但是更復(fù)雜的情況呢?

你知道如下代碼將會發(fā)生什么嗎?

// Ex2_10: Returning a Lambda From a Method
#include <functional>
#include <iostream>

struct Baz {
  std::function<void()> foo() {
    return [=] { std::cout << s << '\n'; };
  }
  std::string s;
};

int main() {
  auto f1 = Baz{"abc"}.foo();
  auto f2 = Baz{"xyz"}.foo();
  f1();
  f2();
}

這段代碼定義了一個(gè) Baz 對象然后去調(diào)用 foo()。請注意 foo() 返回的是一個(gè) lambda(存儲在 std::function)而且它捕獲了這個(gè)類的一個(gè)成員變量。

由于我們使用了臨時(shí)對象,因而無法確定當(dāng)我們調(diào)用 f1 和 f2 的時(shí)候會發(fā)生什么。這是一個(gè)懸垂引用問題,會導(dǎo)致未定義行為。
類似的:

struct Bar {
  std::string const& foo() const { return s; };
  std::string s;
};
auto&& f1 = Bar{"abc"}.foo(); // a dangling reference

如果顯式地聲明捕獲([s]),則會得到編譯器錯(cuò)誤。

std::function<void()> foo() {
  return [s] { std::cout << s << '\n'; };
} // error: 'this' was not captured!

總而言之,當(dāng) lambda 比對象本身活得更久時(shí),捕獲這一點(diǎn)可能會變得棘手。當(dāng)您使用異步調(diào)用或多線程時(shí),可能會發(fā)生這種情況。

我們將在 C++17 的章節(jié)中回到這個(gè)主題。參見“并發(fā)執(zhí)行使用 lambda”。

只移對象

如果你有一個(gè)只移對象(例如 unique_ptr),你無法將它作為一個(gè)捕獲變量移動(dòng)到 lambda 內(nèi)。按值捕獲不起作用,你只能按引用去捕獲。

std::unique_ptr<int> p(new int{10});
auto foo = [p]() {}; // 無法通過編譯
auto foo_ref = [&p]() { }; // 通過編譯,然而,所有權(quán)未能傳遞

在上面的示例中,您可以看到捕獲unique_ptr的唯一方法是通過引用。然而,這種方法可能不是最好的,因?yàn)樗晦D(zhuǎn)移指針的所有權(quán)。

在關(guān)于 C++14 的下一章中,您將看到由于使用初始化器捕獲,這個(gè)問題得到了解決。轉(zhuǎn)到 C++ 14章中的“移動(dòng)”一節(jié),繼續(xù)了解這個(gè)主題。

常量性的保留

如果你捕獲一個(gè)常量,那么常量性會被保留下來:

#include <iostream>
#include <type_traits>

int main() {
  const int x = 10;
  auto foo = [x] () mutable {
    std::cout << std::is_const<decltype(x)>::value << '\n';
    x = 11;
  };
  foo();
}

上面的代碼不能編譯,因?yàn)椴东@的變量是常量。下面是本例可能生成的函數(shù)對象:

struct __lambda_x {
  void operator()() { x = 11; /*error!*/ }
  const int x;
};

捕獲參數(shù)包

為了結(jié)束對捕獲子句的討論,我們應(yīng)該提到您還可以利用可變模板來捕獲。編譯器將包展開為一個(gè)非靜態(tài)數(shù)據(jù)成員列表,如果您想在模板化代碼中使用 lambda,這可能很方便。例如,下面是一個(gè)測試捕獲的代碼示例:

// Ex2_12: Capturing a Variadic Pack.
#include <iostream>
#include <tuple>

template<class... Args>
void captureTest(Args... args) {
  const auto lambda = [args...] {
    const auto tup = std::make_tuple(args...);
    std::cout << "tuple size: " <<
            std::tuple_size<decltype(tup)>::value << '\n';
     std::cout << "tuple 1st: " << std::get<0>(tup) << '\n';
  };
  lambda(); // call it
}

int main() {
  captureTest(1, 2, 3, 4);
  captureTest("Hello world", 10.0f);
}

// 輸出:
// tuple size: 4
// tuple 1st: 1
// tuple size: 2
// tuple 1st: Hello world

這段實(shí)驗(yàn)性的代碼表明,您可以按值捕獲可變參數(shù)包(也可以通過引用),然后將該包“存儲”到元組對象中。然后在元組上調(diào)用一些輔助函數(shù)來訪問它的數(shù)據(jù)和屬性。

你也可以使用 C++ Insights 來查看編譯器是如何生成代碼并將模板、參數(shù)包和 lambda 擴(kuò)展為代碼的。參見這里的示例 @C++Insight。

返回類型

在大多數(shù)情況下,即便是在 C++11 中,你也可以跳過 lambda 的返回類型,然后編譯器可以為您推斷出類型名。

附注:最初,返回類型推導(dǎo)僅限于函數(shù)體中包含單個(gè)返回語句的lambda。然而,這個(gè)限制很快就被取消了,因?yàn)閷?shí)現(xiàn)一個(gè)更方便的版本沒有問題。

總而言之,從 C++11 開始,只要所有的返回語句都是相同的類型,編譯器就能夠推斷出返回類型。

從缺陷報(bào)告中我們可以讀到以下內(nèi)容:

如果lambda表達(dá)式不包含尾隨返回類型,則尾隨返回類型表示以下類型:

  • 如果復(fù)合語句中沒有return語句,或者所有的return語句返回一個(gè)void類型的表達(dá)式,或者沒有表達(dá)式或帶括號的init-list,則類型為void;
  • 否則,如果所有的return語句都返回一個(gè)表達(dá)式以及左值到右值轉(zhuǎn)換(7.3.2 [conv.lval])、數(shù)組到頂層指針轉(zhuǎn)換(7.3.3 [conv.array])和函數(shù)到指針轉(zhuǎn)換(7.3.4 .lval)后返回的表達(dá)式的類型[conv.func])是相同的,即共同類型
  • 否則,程序?yàn)椴∈?/li>
// Ex2_13: Return Type Deduction.
#include <type_traits>

int main() {
  const auto baz = [](int x) noexcept {
  if (x < 20)
    return x * 1.1;
  else
    return x * 2.1;
  };
  static_assert(std::is_same<double, decltype(baz(10))>::value, "has to be the same!");
}

在上面的 lambda 中,我們有兩個(gè)返回語句,但它們都指向 double,因此編譯器可以推斷出類型。

在 C++ 14中,lambda 的返回類型將被更新,以適應(yīng)正則函數(shù)的自動(dòng)類型推導(dǎo)規(guī)則。參見“返回類型推斷”。這樣就得到了一個(gè)更簡單的定義。

尾置返回類型語法

如果你想明確返回類型,可以使用尾隨返回類型說明。例如,當(dāng)你返回一個(gè)字符串字面值時(shí):

#include <iostream>
#include <string>

int main() {
  const auto testSpeedString = [](int speed) noexcept {
    if (speed > 100)
      return "you're a super fast";
    return "you're a regular";
  };
  auto str = testSpeedString(100);
  str += " driver"; // oops! no += on const char*!
  std::cout << str;
  return 0;
}

上面的代碼無法編譯,因?yàn)榫幾g器將 const char* 推斷為 lambda 的返回類型。而字符串字面量上沒有 += 操作符,所以代碼會中斷。

可以通過顯式地將返回類型設(shè)置為 std::string 來解決這個(gè)問題:

auto testSpeedString = [](int speed) -> std::string {
  if (speed > 100)
    return "you're a super fast";
  return "you're a regular";
};
auto str = testSpeedString(100);
str += " driver"; // works fine

請注意,我們現(xiàn)在必須刪除 noexcept,因?yàn)?std::string 創(chuàng)建可能會拋出錯(cuò)誤。
另外,您還可以使用命名空間 std::string_literals; 然后你返回 "you're a regular"s 表示 std::string 類型。

轉(zhuǎn)換成一個(gè)函數(shù)指針

如果你的 lambda 并不捕獲任何變量,那么編譯器可以將其轉(zhuǎn)換成一個(gè)常規(guī)函數(shù)指針。以下是標(biāo)準(zhǔn)對此的詳細(xì)描述:

沒有 lambda 捕獲的 lambda 表達(dá)式的閉包類型具有一個(gè)公共非虛非顯式 const 轉(zhuǎn)換函數(shù),該轉(zhuǎn)換函數(shù)指向與閉包類型的函數(shù)調(diào)用操作符具有相同形參和返回類型的函數(shù)。此轉(zhuǎn)換函數(shù)返回的值應(yīng)為函數(shù)的地址,該函數(shù)在調(diào)用時(shí)與調(diào)用閉包類型的函數(shù)調(diào)用操作符具有相同的效果。

為了說明 lambda 如何支持這種轉(zhuǎn)換,讓我們考慮以下示例。它定義了一個(gè)函數(shù)對象 baz,該對象顯式地定義了轉(zhuǎn)換操作符:

// Ex2_15: Conversion to a Function Pointer. 
#include <iostream>

void callWith10(void(* bar)(int)) { bar(10); }

int main() {
  struct {
    using f_ptr = void(*)(int);
    void operator()(int s) const { return call(s); }
    operator f_ptr() const { return &call; }
  private:
    static void call(int s) { std::cout << s << '\n'; };
  } baz;
  callWith10(baz);
  callWith10([](int x) { std::cout << x << '\n'; });
}

在前面的程序中,有一個(gè)函數(shù) callWith10,它接受一個(gè)函數(shù)指針。然后我們用兩個(gè)參數(shù)調(diào)用它(第 18 行和第 19 行):第一個(gè)使用 baz,它是一個(gè)函數(shù)對象類型,包含必要的轉(zhuǎn)換操作符—它轉(zhuǎn)換為 f_ptr,這與 callWith10 的輸入?yún)?shù)相同。稍后,我們將調(diào)用 lambda 函數(shù)。在這種情況下,編譯器在下面執(zhí)行所需的轉(zhuǎn)換。

當(dāng)需要調(diào)用需要回調(diào)的 C 風(fēng)格函數(shù)時(shí),這種轉(zhuǎn)換可能很方便。例如,下面你可以找到從 C 庫調(diào)用 qsort 并使用 lambda 以相反順序?qū)υ嘏判虻拇a:

// Ex2_16: Calling a C-style function. 
#include <cstdlib>
#include <iostream>

int main () {
  int values[] = { 8, 9, 2, 5, 1, 4, 7, 3, 6 };
  constexpr size_t numElements = sizeof(values)/sizeof(values[0]);
  std::qsort(values, numElements, sizeof(int),
      [](const void* a, const void* b) noexcept {
      return ( *(int*)b - *(int*)a );
    }
  );
  for (const auto& val : values)
  std::cout << val << ", ";
}

如你所見,使用 std::qsort,它只接受函數(shù)指針作為比較器。編譯器可以對我們傳遞的無狀態(tài)lambda 進(jìn)行隱式轉(zhuǎn)換。

棘手的情況

在我們進(jìn)入另一個(gè)話題之前,還有一個(gè)案例可能會很有趣:

// Ex2_17: Plus and a Lambda.
#include <type_traits>

int main() {
  auto funcPtr = +[]{};
  static_assert(std::is_same<decltype(funcPtr), void (*)()>::value);
}

請注意 + 的奇怪語法。如果刪除加號,則 static_assert 失敗。為什么呢?
為了理解它是如何工作的,我們可以看看 C++ Insights 項(xiàng)目生成的輸出。

using FuncPtr_4 = void (*)();
FuncPtr_4 funcPtr = +static_cast<void (*)()>(__la.operator __la::retType_4_18());
/* PASSED: static_assert(std::integral_constant<bool, 1>::value); */
// __la is __lambda_4_18 in cppinsights

代碼使用 +,這是一個(gè)一元操作符。該操作符可以操作指針,因此編譯器將無狀態(tài)lambda轉(zhuǎn)換為函數(shù)指針,然后將其賦值給funcPtr。

另一方面,如果去掉加號,那么 funcPtr 就只是一個(gè)普通的閉包對象,這就是 static_assert 失敗的原因。

雖然用“+”來編寫這樣的語法可能不是最好的主意,但是如果用 static_cast,效果是一樣的。在不希望編譯器創(chuàng)建太多函數(shù)實(shí)例化的情況下,可以應(yīng)用此技術(shù)。例如:

// Ex2_18: Casting to a Function Pointer. 
template<typename F>
void call_function(F f) { f(10); }

int main() {
  call_function(static_cast<int (*)(int)>([](int x){return x + 2;}));
  call_function(static_cast<int (*)(int)>([](int x){return x * 2;}));
}

在上面的例子中,編譯器只需要?jiǎng)?chuàng)建一個(gè) call_function 的實(shí)例,因?yàn)樗唤邮芤粋€(gè)函數(shù)指針 int (*)(int)。但是如果你去掉 static_cast,那么你將得到兩個(gè)版本的 call_function,因?yàn)榫幾g器必須為 lambdas 創(chuàng)建兩個(gè)不同的類型。

IIFE —— 立即調(diào)用的函數(shù)表達(dá)式

到目前為止,在您看到的大多數(shù)示例中,您可以注意到我定義了一個(gè) lambda,然后在稍后調(diào)用它。

然而,你也可以立即調(diào)用 lambda:

// Ex2_19: Calling Lambda Immediately. 
#include <iostream>

int main() {
  int x = 1, y = 1;
  [&]() noexcept { ++x; ++y; }(); // <-- call ()
  std::cout << x << ", " << y;
}

正如你在上面看到的,lambda 被創(chuàng)建并且沒有被賦值給任何閉包對象。然后用()調(diào)用它。如果您運(yùn)行這個(gè)程序,你可以期望看到 2, 2 作為輸出。當(dāng)你對 const 對象進(jìn)行復(fù)雜的初始化時(shí),這種表達(dá)式可能很有用。

const auto val = []() {
  /* several lines of code... */
}(); // call it!

以上代碼中,val 是一個(gè)由 lambda 表達(dá)式返回的某種類型的常量。例如:

// val1 is int
const auto val1 = []() { return 10; }();
// val2 is std::string
const auto val2 = []() -> std::string { return "ABC"; }();

下面你可以找到一個(gè)更長的例子,我們使用 IIFE 作為輔助 lambda 來在函數(shù)中創(chuàng)建一個(gè)常量值:

// Ex2_20: IIFE and HTML Generation.
#include <iostream>
#include <string>

void ValidateHTML(const std::string&) { }

std::string BuildAHref(const std::string& link, const std::string& text) {
  const std::string html = [&link, &text] {
    const auto& inText = text.empty() ? link : text;
    return "<a href=\"" + link + "\">" + inText + "</a>";
  }(); // call!
  ValidateHTML(html);
  return html;
}

int main() {
  try {
    const auto ahref = BuildAHref("www.leanpub.com", "Leanpub Store");
    std::cout << ahref;
  } catch (...) {
    std::cout << "bad format...";
  }
}

上面的例子包含一個(gè)函數(shù) BuildAHref,它接受兩個(gè)參數(shù),然后構(gòu)建一個(gè) <a> </a> HTML 標(biāo)記?;谳斎?yún)?shù),我們構(gòu)建html變量。如果文本不為空,則使用它作為內(nèi)部 HTML 值。否則,我們使用鏈接。我們希望 html 變量為 const,但是很難編寫具有輸入?yún)?shù)所需條件的緊湊代碼。多虧了 IIFE,我們可以編寫一個(gè)單獨(dú)的 lambda,然后用 const 標(biāo)記變量。稍后,可以將該變量傳遞給ValidateHTML。

關(guān)于可讀性的一個(gè)注意事項(xiàng)

有時(shí),立即調(diào)用 lambda 可能會導(dǎo)致一些可讀性問題。例如:

const auto EnableErrorReporting = [&]() {
  if (HighLevelWarningEnabled())
    return true;
  if (MidLevelWarningEnabled())
    return UsersWantReporting(); // depends on user settings...
  return false;
}();

if (EnableErrorReporting) {
// ...
}

在上面的示例中,lambda 代碼非常復(fù)雜,閱讀代碼的開發(fā)人員不僅要破譯 lambda 是立即調(diào)用的,還要推斷EnableErrorReporting 類型。他們可能會假設(shè) EnableErrorReporting 是閉包對象,而不僅僅是一個(gè) const 變量。對于這種情況,您可以考慮不使用 auto,以便我們可以很容易地看到類型。甚至可以在 }() 旁邊添加注釋,比如 // 立即調(diào)用。

關(guān)于升級版的 IIFE,你可以在 C++17 章節(jié)了解到更多。

從 lambda 繼承

這可能令人驚訝,但您確實(shí)可以從 lambda 派生!

由于編譯器使用 operator() 將 lambda 表達(dá)式展開為函數(shù)對象,因此我們可以從這個(gè)類型繼承。

來看一個(gè)基礎(chǔ)的例子:

// Ex2_21: Inheriting from a single Lambda. 
#include <iostream>

template<typename Callable>
class ComplexFn : public Callable {
public:
  explicit ComplexFn(Callable f) : Callable(f) {}
};

template<typename Callable>
ComplexFn<Callable> MakeComplexFunctionObject(Callable&& cal) {
  return ComplexFn<Callable>(std::forward<Callable>(cal));
}

int main() {
  const auto func = MakeComplexFunctionObject([]() {
    std::cout << "Hello Complex Function Object!";
  });
  func();
}

在這個(gè)例子中,ComplexFn 類是從 Callable 派生出來的,Callable 是一個(gè)模板參數(shù)。如果我們想從 lambda 中派生,我們需要一點(diǎn)小技巧,因?yàn)槲覀儾荒芷磳懗鲩]包類型的確切類型(除非我們將其包裝到 std::function 中)。

這就是為什么我們需要 MakeComplexFunctionObject 函數(shù)來執(zhí)行模板參數(shù)推導(dǎo)并獲得 lambda 閉包的類型。

除了它的名字,ComplexFn 只是一個(gè)簡單的包裝器,沒有太多的用途。這樣的代碼模式有什么用例嗎?

例如,我們可以擴(kuò)展上面的代碼,從兩個(gè) lambdas 繼承并創(chuàng)建一個(gè)重載集合:

// Ex2_22: Inheriting from two Lambdas. 
#include <iostream>

template<typename TCall, typename UCall>
class SimpleOverloaded : public TCall, UCall {
public:
  SimpleOverloaded(TCall tf, UCall uf) : TCall(tf), UCall(uf) {}
  using TCall::operator();
  using UCall::operator();
};

template<typename TCall, typename UCall>
SimpleOverloaded<TCall, UCall> MakeOverloaded(TCall&& tf, UCall&& uf) {
  return SimpleOverloaded<TCall, UCall>(std::forward<TCall> tf, std::forward<UCall> uf);
}

int main() {
 const auto func = MakeOverloaded(
    [](int) { std::cout << "Int!\n"; },
    [](float) { std::cout << "Float!\n"; }
  );
  func(10);
  func(10.0f);
}

這次我們有更多的代碼:我們從兩個(gè)模板形參派生,但是我們還需要顯式地公開它們的調(diào)用操作符。

為什么呢?這是因?yàn)樵趯ふ艺_的函數(shù)重載時(shí),編譯器要求候選函數(shù)在相同的作用域內(nèi)。

為了理解這一點(diǎn),讓我們編寫一個(gè)從兩個(gè)基類派生的簡單類型。該示例還注釋掉了兩個(gè) using 語句:

// Ex2_23: Deriving from two classes, error. 
#include <iostream>
struct BaseInt {
  void Func(int) { std::cout << "BaseInt...\n"; }
};

struct BaseDouble {
  void Func(double) { std::cout << "BaseDouble...\n"; }
};

struct Derived : public BaseInt, BaseDouble {
  //using BaseInt::Func;
  //using BaseDouble::Func;
};

int main() {
  Derived d;
  d.Func(10.0);
}

我們有兩個(gè)實(shí)現(xiàn) Func 的基類。我們想從派生對象調(diào)用那個(gè)方法。

GCC 報(bào)告以下錯(cuò)誤:
error: request for member 'Func' is ambiguous
因?yàn)槲覀冏⑨尩袅?using 語句 ::Func() 可以來自 BaseInt 或 BaseDouble 的作用域。編譯器有兩個(gè)作用域來搜索最佳候選,根據(jù)標(biāo)準(zhǔn),這是不允許的。

那么,讓我們回到我們的主要用例:SimpleOverloaded 是一個(gè)基本類,它還不能用于生產(chǎn)環(huán)境。請參閱 C++17 章節(jié),在那里我們將討論該模式的高級版本。由于C++17 的一些特性,我們將能夠從多個(gè) lambdas 繼承(多虧了可變模板)并語法更加緊湊。

存儲 lambda 到容器

作為本章的最后一項(xiàng)技術(shù),讓我們看一下在容器中存儲閉包的問題。

但是我不是寫過不能默認(rèn)創(chuàng)建和賦值 lambda 嗎?

是的,但是,我們可以在這里做一些戲法。

其中一種技術(shù)是利用無狀態(tài) lambda 轉(zhuǎn)換為函數(shù)指針的屬性。雖然不能直接存儲閉包對象,但可以保存從 lambda 表達(dá)式轉(zhuǎn)換而來的函數(shù)指針。

例如:

// Ex2_24: Storing Lambdas As Function Pointers. 
#include <iostream>
#include <vector>

int main() {
  using TFunc = void (*)(int&);
  std::vector<TFunc> ptrFuncVec;

  ptrFuncVec.push_back([](int& x) { std::cout << x << '\n'; });
  ptrFuncVec.push_back([](int& x) { x *= 2; });
  ptrFuncVec.push_back(ptrFuncVec[0]); // print it again;

  int x = 10;
  for (const auto &entry : ptrFuncVec)
    entry(x);
}

在上面的例子中,我們創(chuàng)建了用來存儲變量的函數(shù)指針的向量。容器中有三個(gè)條目:

  • 第一個(gè)打印輸入變量的值。
  • 第二個(gè)更改它的值。
  • 第三個(gè)復(fù)制自第一個(gè),因此它也打印值。

以上解決方案可以工作,但僅限于無狀態(tài) lambda。如果我們想解除這個(gè)限制呢?
為了解決這個(gè)問題,我們可以使用求助于 std::function。為了使示例更有趣,它還以簡單的整數(shù)轉(zhuǎn)換為處理 std::string 對象的lambda 為例:

// Ex2_25: Storing Lambdas As std::function. 
#include <algorithm>
#include <functional>
#include <iostream>
#include <vector>

int main() {
  std::vector<std::function<std::string(const std::string&)>> vecFilters;
  size_t removedSpaceCounter = 0;
  const auto removeSpaces = [&removedSpaceCounter](const std::string& str) {
    std::string tmp;
    std::copy_if(str.begin(), str.end(), std::back_inserter(tmp),
                 [](char ch) {return !isspace(ch); });
    removedSpaceCounter += str.length() - tmp.length();
    return tmp;
  };

  const auto makeUpperCase = [](const std::string& str) {
    std::string tmp = str;
    std::transform(tmp.begin(), tmp.end(), tmp.begin(),
                   [](unsigned char c){ return std::toupper(c); });
    return tmp;
  };

  vecFilters.emplace_back(removeSpaces);
  vecFilters.emplace_back([](const std::string& x) {return x + " Amazing"; });
  vecFilters.emplace_back([](const std::string& x) {return x + " Modern"; });
  vecFilters.emplace_back([](const std::string& x) {return x + " C++"; });
  vecFilters.emplace_back([](const std::string& x) {return x + " World!"; });
  vecFilters.emplace_back(makeUpperCase);

  const std::string str = " H e l l o ";
  auto temp = str;
  for (const auto &entryFunc : vecFilters)
    temp = entryFunc(temp);
  std::cout << temp;
  std::cout <<"\nremoved spaces: " << removedSpaceCounter << '\n';
}
// 輸出:
// HELLO AMAZING MODERN C++ WORLD!
// removed spaces: 12

這次我們將 std::function<std::string(const std::string&)> 存儲在容器中。

這允許我們使用任何類型的函數(shù)對象,包括帶有捕獲變量的 lambda 表達(dá)式。其中一個(gè) lambda removeSpacesCnt 捕獲一個(gè)變量,該變量用于存儲有關(guān)從輸入字符串中刪除的空格的信息。

總結(jié)

在本章中,您學(xué)習(xí)了如何創(chuàng)建和使用 lambda 表達(dá)式。我描述了語法、捕獲子句、lambda 的類型,并介紹了許多示例和用例。我們甚至更進(jìn)一步,我給你們展示了一種派生自 lambda 的模式或者把它存儲在一個(gè)容器里。

但這還不是全部!

Lambda 表達(dá)式已經(jīng)成為現(xiàn)代 C++ 的重要組成部分。有了更多的用例,開發(fā)人員也看到了改進(jìn)這個(gè)特性的可能性。這就是為什么你現(xiàn)在可以轉(zhuǎn)到下一章,看看 ISO 委員會在 C++14 中添加的重要更新。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容