接下來的文章我們要再次討論 C++ 11 引入的 lambda 表達式, 其中有一些內容需要大家對 C++ 14 以及之後的 lambda 表達式變動有一些了解, 所以我撰寫了這篇導讀合集

C++ 14 Proposal N3648《Wording Changes for Generalized Lambda-capture》導讀

這篇 Paper 的背景其實是被視為 C++ 11 遺憾的 lambda 表達式移動捕獲. C++ 11 引入的 lambda 表達式可以對一定可視範圍之內的變數進行值捕獲或者參考捕獲, 但是由於時間關係, 移動捕獲沒有被納入 C++ 11. 不過這群大神們想出了比移動捕獲引入 C++ 更加有用的泛型 lambda 捕獲列表 :

auto y {1};
auto lambda {[x {1}, y] {}};

在 lambda 被宣告之前, 從未出現過一個名稱為 x 的變數, 也就是說, 在捕獲列表內, x 是以初始化的方式先宣告, 然後被捕獲. 這確實比只引入移動捕獲強大, 有了泛型捕獲列表, 我們可以這樣進行移動捕獲 :

#include <vector>

using namespace std;
int main(int argc, char *argv[]) {
    vector<int> vec {1, 2, 3, 4, 5};
    auto lambda {[vec {move(vec)}] {
        //...
    }};
}

左側和右側的 vector 的名稱都為 vec, 這是允許的. 左側的 vector 其生存週期僅限於 lambda 表達式之內, 相當於在一個小的作用範圍內宣告一個新的同名變數以覆蓋外圍名稱空間中的名稱. 而右側的 vector 是被捕獲的 vector, 它的生存週期是在函式 main 之內

關於生存週期的問題, lambda 表達式是比較複雜的. 一般地, 在 lambda 表達式內部宣告的變數在 lambda 被呼叫終結的時候就會被回收. 但是捕獲列表內的變數則不同, 如果某個變數在 lambda 表達式的捕獲列表中初始化, 那麼它將在對應的作用範圍結束的時候被回收. 說到底, 其實宣告一個 lambda 表達式就是一個不具名類別對應的物件. 但是對於初始化列表則不同, 它會在 lambda 表達式初始化結束的時候就被回收 :

#include <iostream>

using namespace std;
class Foo {
    int *p;
public:
    Foo() : p {new int {123}} {
        cout << "construct" << endl;
    }
    Foo(const Foo &) : p {new int {111}} {
        cout << "copy" << endl;
    }
    Foo(Foo &&rhs) noexcept : p {rhs.p} {
        cout << "move" << endl;
        rhs.p = nullptr;
    }
    ~Foo() noexcept {
        cout << "destruct" << endl;
        delete this->p;
    }
    void print() const {
        cout << *this->p << endl;
    }
};
int main(int argc, char *argv[]) {
    auto lambda {[x {Foo()}]() -> const Foo & {
        return x;
    }};
    auto &f {lambda()};
    f.print();
}

上述程式碼的輸出為 :

construct
123
destruct

但是為捕獲列表中的 x 之後加個等號就不同了 :

int main(int argc, char *argv[]) {
    auto lambda {[x = {Foo()}]() -> const Foo & {
        return x.begin()[0];
    }};
    auto &f {lambda()};
    f.print();
}

這次的輸出就變成了 :

construct
destruct
123

在成員函式 print 被呼叫之前, x 中的第一個物件就已經被回收了. 所以即使後來得到了 123, 那也只是未定行為, Apple Clang 給我們提供了便利而已. MSVC 可就沒那麼幸運了, 你可能只會得到一個莫名其妙的數字

C++ 17 Proposal P0170R1 《Wording for Constexpr Lambda》導讀

這篇 Proposal 是希望 lambda 表達式也可以是 constexpr 的, 總共有 5 種表現形式

允許 lambda 表達式出現在 constexpr 函式內 :

constexpr int add(int i) {
    return [i] {
        return i + 1;
    }();
}
static_assert(add(1) == 2, "compile error!");       //OK

允許 lambda 表達式被 constexpr 標識 :

constexpr auto add {[](int i) noexcept -> int {
    return i + 1;
}};
static_assert(add(1) == 2, "compile_error!");       //OK

constexpr 也可以出現在原來 mutablenoexcept 出現的位置 :

auto add {[](int i) constexpr noexcept -> int {
    return i + 1;
}};
static_assert(add(1) == 2, "compile_error!");       //OK

mutableconstexpr 出現的位置可以互換, 但是 noexcept 必定是出現在最後的

甚至只要編碼器檢測到可以進行編碼期計算, 那麼沒有被 constexpr 標識的 lambda 表達式也可以產生編碼期常數 :

auto add {[](int i) noexcept -> int {
    return i + 1;
}};
static_assert(add(1) == 2, "compile_error!");       //OK

允許 lambda 表達式向被 constexpr 標識的指標轉型 :

auto add {[](int i) noexcept -> int {
    return i + 1;
}};
constexpr int (*fp)(int) {add};
static_assert(fp(1) == 2, "compile_error!");       //OK

這個特性在普通的函式上暫時無法做到

C++ 17 Proposal P0018R3 《Lambda Capture of *this by Value as [=,*this]》導讀

在 C++ 17 之前, 若 lambda 表達式要捕獲 *this, 不論你是通過值捕獲還是參考捕獲, 最終的結果都是以參考的方式捕獲 *this :

#include <iostream>

struct Foo {
    int a {0};
    void func() {
        auto lambda {[=]() mutable {
            this->a = 111;
        }};
        lambda();
    }
};

using namespace std;
int main(int argc, char *argv[]) {
    Foo f;
    f.func();
    cout << f.a << endl;        //輸出結果 : 111
}

這篇 Proposal 提出要通過值的方式捕獲 *this. 但是 C++ 中捕獲的方式似乎已經被用盡了, 也沒有什麼其它比較好的語法了. 最後, 這篇 Proposal 提出使用 [=, *this] 這種方式進行值捕獲 :

#include <iostream>

struct Foo {
    int a {0};
    void func() {
        auto lambda {[=, *this]() mutable {
            this->a = 111;
        }};
        lambda();
    }
};

using namespace std;
int main(int argc, char *argv[]) {
    Foo f;
    f.func();
    cout << f.a << endl;        //輸出結果 : 0
}

除此之外, 配合 C++ 14 引入的廣義 lambda 捕獲列表, 可以給值捕獲的 *this 起個名字. 此時, 使用 tmp*this 沒有影響 :

#include <iostream>

struct Foo {
    int a {0};
    void func() {
        auto lambda {[=, tmp = *this]() mutable {
            tmp.a = 111;
        }};
        lambda();
    }
};

using namespace std;
int main(int argc, char *argv[]) {
    Foo f;
    f.func();
    cout << f.a << endl;        //輸出結果 : 0
}

但是若在 lambda 表達式內使用 this, 就是在說明我想進行值捕獲的同時, 也要參考捕獲 *this :

#include <iostream>

struct Foo {
    int a {0};
    void func() {
        auto lambda {[=, tmp = *this]() mutable {
            tmp.a = 111;
            this->a = 111;
        }};
        lambda();
    }
};

using namespace std;
int main(int argc, char *argv[]) {
    Foo f;
    f.func();
    cout << f.a << endl;        //輸出結果 : 111
}

C++ 20 Proposal P0806R2 《Deprecate implicit capture of this via [=]》導讀

對應地, C++ 20 中提出了棄用隱含捕獲 [=] 的方式參考捕獲 this

C++ 20 Proposal P0409R2 《Allow lambda capture [=, this]》導讀

在 C++ 20 之後, 若想通過隱含捕獲 [=] 的方式參考捕獲 this, 那麼需要明確寫出, 也就是以 [=, this] 的形式進行捕獲

C++ 20 Proposal P0428R2 《Familiar template syntax for generic lambdas》導讀

在 C++ 14 引入泛型 lambda 表達式的時候, 所有的泛型 lambda 表達式都是通過 auto 的方式進行宣告, 這就造成了一些不太方便的地方 :

  1. 泛型 lambda 表達式可以接受任何型別的引數. 若想要明確拒絕, 只能通過 static_assert 或者其它方式強行產生編碼錯誤
  2. 要獲取引數的具體型別只能通過 decltype 進行推導, 這導致了諸多不便之處
  3. 完美轉遞也需要借助 decltype 來完成

為了解決這些問題, 這篇 Proposal 提出為 lambda 表達式引入 template 語義 :

#include <vector>

const auto lambda {[]<typename T, typename U, typename ...Ts>(const T &t, std::vector<U> &, Ts &&args) -> auto {
    //...
}};

所有的樣板參數寫在捕獲列表之後, 函式參數列表之前

對於問題 1, 若我們想要接受 std::vector 型別的物件, 那麼只能通過這種方式 :

#include <vector>

using namespace std;
template <typename T>
struct is_std_vector : false_type {};
template <typename T>
struct is_std_vector<vector<T>> : true_type {};
const auto lambda {[](auto &&vec) -> auto {
    static_assert(is_std_vector<typename decay<decltype(vec)>::type>::value, "The type of argument must be std::vector!");
    //...
}};

這種強行編碼錯誤的方式, 和 C++ 的 SFINAE 思想不太相符. 但是現在, 可以直接宣告接受任意 std::vector 特製化的型別 :

#include <vector>

using namespace std;
template <typename T>
struct is_std_vector : false_type {};
template <typename T>
struct is_std_vector<vector<T>> : true_type {};
const auto lambda {[]<typename T>(vector<T> &) -> auto {
    //...
}};

對於問題 2, 如果我們要獲取一個型別中保存的型別別名, 原來你需要這樣做 :

#include <type_traits>

const auto lambda {[](const auto &value) -> auto {
    using value_type = typename std::decay<decltype(value)>::type::value_type;
    //...
}}

如果沒有 std::decay 的幫助, 我們根本不可能取得一個類別內部保存的型別別名, 因為 value 最終的推導值是一個參考型別, 而且帶有 const 限定. 一個參考型別內部沒有任何型別別名. 在 C++ 20 之後, 我們只需要這樣做 :

const auto lambda {[]<typename T>(const T &value) -> auto {
    using value_type = typename T::value_type;
    //...
}};

對於問題 3, 在 C++ 20 之前, 我們需要這樣做 :

#include <utility>

const auto lambda {[](auto f, auto &&...args) -> auto {
    f(std::forward<decltype(args)>(args)...);
}};

在 C++ 20 之後, 我們不再需要 decltype 的幫助 :

#include <utility>

const auto lambda {[]<typename ...Args>(auto f, Args &&...args) -> auto {
    f(std::forward<Args>(args)...);
}};

C++ 20 Proposal P0624R2 《Default constructible and assignable stateless lambdas》導讀

在進行這篇 Proposal 的導讀之前, 我首先要介紹一下在 C++ 中什麼是無狀態物件. 無狀態物件是指其對應型別是一個類別, 但是類別中不存在任何非靜態成員變數. 這個概念一般用在配置器 Allocator 和 lambda 表達式上. 對於一個 Allocator 來說, C++ 標準樣板程式庫中的 Allocator 就是一個無狀態的 Allocator, 因為裡面不存在任何非靜態成員變數. 如果把 std::vector 也當作一個 Allocator, 它自己維護了一部分記憶體, 那麼 std::vector 就是有狀態的 Allocator, 因為它至少要維護三個指標 :

template <typename T>
class simple_vector {
private:
    T *first;
    T *cursor;
    T *last;
};

first 用於保存頭部記憶體位址, 主要用於回收; cursor 用於標識已使用的範圍; last 用於標識記憶體位址結束之處, 它可以用一個保存記憶體大小的變數來替代. 這就是一個有狀態的物件

對於 lambda 表達式來說也是一樣, 任意一個 lambda 表達式實際上編碼器都會為其生成一個不具名類別. 在 C++ 17 之前, 編碼器並不會為 lambda 所對應的類別生成預設建構子和指派操作. 也就是說, 在 C++ 17 之前, 以下程式碼是不能通過編碼的 :

#include <map>

int main(int argc, char *argv[]) {
    auto lambda {[](auto x, auto y) {
        return x < y;
    }};
    decltype(lambda) l;     //Compile Error
    std::map<std::string, int, decltype(lambda)> m1, m2;        //Compile Error
    m1 = m2;        //Compile Error
}

這篇 Proposal 提出為無狀態的 lambda 表達式生成預設建構子和指派操作, 那麼上述程式碼在 C++ 20 之後就可以通過編碼

C++ 20 Proposal P0315R4 《Wording for lambdas in unevaluated contexts》導讀

我們已經知道, 每一個 lambda 表達式都有自己獨一無二的型別, 即使這些 lambda 的內容完全一樣, 這個型別是由編碼器生成的 :

auto lambda1 {[]() {}};
auto lambda2 {[]() {}};
static_assert(is_same<decltype(lambda1), decltype(lambda2)>::value, "Different type!");     //Compile Error : Different type!

放開大腦, 有些人會寫下以下程式碼 :

void func(decltype([]() {}));
void func(decltype([]() {})) {
    //...
}

那麼對於函式 func 來說, 到底算是多載還是先宣告後定義呢? 對於這樣的語境, 我們稱為不可估量語境 (unevaluated-context). 這樣的程式碼在 C++ 20 之前是無法通過編碼的. 但是, 這也限制了下面這種情況 :

我們曾在文章《【C++ 14 Paper 導讀】函式回傳型別推導》中說過通過編碼器推導函式回傳型別實現 std::tuple 元素排序 :

#include <tuple>

template <typename F, typename ...Ts>
auto sort(std::tuple<Ts...>, F);

現在, 我們希望得到函式 sort 的回傳型別, 那麼我們會使用 decltype :

#include <tuple>

template <typename F, typename ...Ts>
auto sort(std::tuple<Ts...>, F);
using sorted_tuple = decltype(sort(std::make_tuple(1, 'l', 0.0f, U'0'), [](auto a, auto b) noexcept {
    return a < b;
}));

由於 auto 的型別可能和 lambda 表達式的型別有關, 於是編碼器會告訴你這是一個不可估量語境, 於是你得到了一個編碼錯誤. 但是這篇 Proposal 提出了將這個限制解除, 因此在 C++ 20 之後, 上述程式碼可以通過編碼, 也包括之前那個. 但並不止於此, 這引發了一個更深入的討論 : 是否存在帶狀態的編碼期程式設計 (在 C++ 中, 這被稱為 Stateful Meta-Programming). 由於每一個 lambda 表達式都有一個不同的型別, 那麼我是否可以通過類別樣板生成型別標記?

#include <type_traits>

template <void () = []() {}>
struct unique {};
static_assert(std::is_same<unique<>, unique<>>::value, "Not same type!");       //Compile Error : Not same type

C++ 20 Proposal P0780R2 《Allow pack expansion in lambda init-capture》導讀

在 C++ 20 之前, lambda 表達式要想捕獲一個引數包, 可以通過值捕獲或者參考捕獲的方式 :

template <typename ...Args>
void v_capture(Args &&...args) {
    auto lambda {[args...] {
        //...
    }};
    //...
}
template <typename ...Args>
void r_capture(Args &&...args) {
    auto lambda {[&args...] {
        //...
    }};
    //...
}

但是由於 C++ 並沒有引入移動捕獲, 因此要想對引數包進行移動捕獲, 只能借助 std::tuple :

#include <tuple>

template <typename ...Args>
void move_capture(Args &&...args) {
    auto lambda {[t = std::make_tuple(std::move(args)...)] {
        //...
    }};
}

這篇 Proposal 提出, 泛型 lambda 捕獲列表可以通過宣告一個引數包的形式來捕獲外層的引數包 :

template <typename ...Args>
void capture(Args &&...args) {
    auto lambda {[...args = args...)] {
        //...
    }};
}

當然, 如果想要通過參考捕獲的方式, 你需要這樣寫 (P2095R0 提出) :

template <typename ...Args>
void capture(Args &&...args) {
    auto lambda {[&...args = args...)] {
        //...
    }};
}