摘要訊息 : 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 表達式 :
template <typename T>
auto lambda = [](T value) -> T {
return ++value;
};
除了使用樣板來宣告 Lambda 表達式之外, C++ 14 中還允許使用 auto
來宣告一個泛型 Lambda 表達式 :
auto lambda = [](auto value) -> T {
return ++value;
};
這兩個宣告的區別就在於使用 auto
宣告的泛型 Lambda 表達式可以出現在函式之內, 但是使用樣板宣告的泛型 Lambda 表達式不可以出現在函式中. 因為函式中本身也不允許出現樣板的宣告.
如果一個泛型 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 表達式, 編碼器生成的名稱未知的類別大概是這樣的 :
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)
這樣的函式指標型別轉型 :
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)
無法直接指向多載的函式呼叫運算子. 因此, 我們除了需要宣告一個型別別名之外, 還需要增加一個靜態成員函式 :
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 之後導致的有趣的結果 —— “選擇性”捕獲 :
#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. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :