摘要訊息 : C++ 14 引入了泛型 Lambda 表達式.

0. 前言

C++ 11 引入了 Lambda 表達式, 使得 C++ 的函數式程式設計更加成熟. 然而泛型的思想在 C++ 中所佔的地位非常高, 所以有人覺得目前的 Lambda 表達式還不足以滿足所有的需求.

N3649《Generic (Polymorphic) Lambda Expression》N3559 提出了泛型 Lambda 表達式 (也被稱為動態 Lambda 表達式).

更新紀錄 :

  • 2022 年 4 月 17 日進行第一次更新和修正.

1. 泛型 Lambda 表達式

在 C++ 14 中, 我們也可以像宣告函式樣板那樣宣告一個 Lambda 表達式 :

Code 1. 採用樣板的 Lambda 表達式
template <typename T> auto lambda = [](T value) -> T { return ++value; };

除了使用樣板來宣告 Lambda 表達式之外, C++ 14 中還允許使用 auto 來宣告一個泛型 Lambda 表達式 :

Code 2. 採用 auto 的 Lambda 表達式
auto lambda = [](auto value) -> T { return ++value; };

這兩個宣告的區別就在於使用 auto 宣告的泛型 Lambda 表達式可以出現在函式之內, 但是使用樣板宣告的泛型 Lambda 表達式不可以出現在函式中. 因為函式中本身也不允許出現樣板的宣告.

如果一個泛型 Lambda 表達式的捕獲列表為空, 那麼我們就可以使用指標指向它們 :

Code 3. 指標指向泛型 Lambda 表達式
int main(int argc, char *argv[]) { auto lambda = [](auto value) -> auto { return value; }; int (*p)(int) = lambda; }

這裡我們要特別指出, 我們不能再用 auto 或者 decltype 來推導 p 的函式指標型別. 如果我們宣告為 auto p = lambda;, 這樣只會得到一個 lambda 的複製物件, 而不是一個函式指標. 若一個泛型 Lambda 表達式中的捕獲列表不為空, 那麼就不能使用指標指向它們, 否則就會產生編碼錯誤.

2. 編碼器如何處理泛型 Lambda 表達式?

和 Lambda 表達式相同, 編碼器會為泛型 Lambda 表達式生成一個名稱未知的類別. 對於 Code 1 或者 Code 2 中的泛型 Lambda 表達式, 編碼器生成的名稱未知的類別大概是這樣的 :

Code 4. 編碼器為泛型 Lambda 表達式生成的類別範例
struct __lambda_class_for_lambda { template <typename T> T operator()(T value) const { return ++value; } // other functions... };

我們從第 1 節中, 我們發現編碼器為 Lambda 表達式生成的類別還有向函式指標轉型的能力. 所以, __lambda_class_for_lambda 中還應該有一個多載的轉型運算子, 向 T (*)(T) 這樣的函式指標型別轉型 :

Code 5. 加入了轉型的合成類別
struct __lambda_class_for_lambda { template <typename T> T operator()(T value) const { return ++value; } template <typename T> operator T (*)(T)() const { return &this->operator(); } };

但是 Code 5 顯然是不正確的, 因為向函式指標轉型中有太多括號, 編碼器沒有辦法分辨哪一個括號才是參數列表. 因此, 向函式指標型別轉型的多載函式必須是一個型別別名. 另外, T (*)(T) 無法直接指向多載的函式呼叫運算子. 因此, 我們除了需要宣告一個型別別名之外, 還需要增加一個靜態成員函式 :

Code 6. 正確的合成類別
struct __lambda_class_for_lambda { private: template <typename T> static T __invoke(T value) { return ++value; } template <typename T> using __pointer_to_this = T (*)(T); public: template <typename T> T operator()(T value) const { return ++value; } template <typename T> operator __pointer_to_this<T>() const { return &__invoke; } };

與往常一樣, 多載的函式呼叫運算子必須被 const 所標識. 另外, 編碼器會在內部生成一個和多載的函式呼叫運算子功能一樣的函式, 這是用於後面回傳指標所服務的. 這個函式必須為 static 函式, 否則指標的型別將會是類別成員函式指標而並非普通的函式指標. 最後, 編碼器會生成一個非 explicit 非虛擬的多載轉型運算子, 和多載函式呼叫運算子一樣, 它也必須被 const 所標識.

3. 有趣的玩法

引入泛型 Lambda 表達式之後, 也就有了更多玩法. 下面有一個實例, 這是由於引入泛型 Lambda 之後導致的有趣的結果 —— “選擇性”捕獲 :

Code 7. 選擇性捕獲
#include <type_traits> struct X { X(int); operator int() const; }; template <bool C> constexpr typename std::conditional<C, int, X>::type f(typename std::conditional<C, int, const int &>::type arg) { return arg; } int main(int argc, char *argv[]) { const auto x = 42; auto g = [](auto a) { const auto i {f<sizeof a == sizeof(char)>(x)}; }; g('a'); // OK g(0); // Error }

我們直接分析 g('a');. 此時, sizeof a == sizeof(char) 成立, 所以 typename std::conditional<true, int, X>::typeint, 而 typename std::conditional<true, int, const int &>::type 也是 int. 最終函式 f 被具現化為 constexpr int f(int arg);. Lambda 表達式中的 x 是在外層是靜態儲存的, 所以在 Lambda 表達式之內可以直接使用. 於是 Lambda 表達式將 x 作為引數傳遞給具現化之後的 f, f 的參數 arg 會為 x 產生一個複製產生的副本.

對於 g(0), sizeof a == sizeof(char) 不成立, 所以 typename std::conditional<false, int, X>::typeX, typename std::conditional<false, int, const int &>::typeconst int &. 最終函式 f 被具現化為 constexpr X f(const int &arg);. Lambda 表達式中的 x 雖然是靜態儲存的, 但是傳遞給函式 f 的時候是以參考的形式繫結到 x 上的. 所以不論 x 是否以靜態儲存, 必須被捕獲, 所以導致了編碼錯誤.

利用這一特性, 我們可以製作一個類似於 static_assert 的功能.

4. 評論

從這個特性引入開始, auto 在 C++ 中開始有了佔位的功能. 所以, 歡迎使用 auto++ 語言 :

auto auto = [auto](auto, auto, ...) -> auto {
    // ...
    return auto;
};