摘要訊息 : 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>::type
是 int
, 而 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>::type
是 X
, typename std::conditional<false, int, const int &>::type
是 const 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;
};
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :