摘要訊息 : 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>::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;
};