摘要訊息 : C++ 14 Proposal N3649 / N3559《Generic (Polymorphic) Lambda Expression》導讀

C++ 14 Proposal N3649 / N3559《Generic (Polymorphic) Lambda Expression》導讀

C++ 11 之後, 引入了 Lambda 表達式, 使得 C++ 的函數式程式設計更加成熟. 但是泛型的思想在 C++ 中所佔的地位非常高, 所以人們覺得目前的 Lambda 表達式還不足以滿足所有的需求, 於是就有了泛型的 Lambda 表達式 (也被稱為動態的 Lambda 表達式)

在這之前, 我需要引伸一些額外的知識 :

我們曾經在 《C++ 學習筆記》中提過, 要使用 Lambda 外部的局域變數, 必須要進行捕獲. 但是, Lambda 之外的靜態變數無需捕獲就可以直接使用了. 這裡的靜態變數是一個比較大的概念, 並非僅僅說被 static 標識的變數, 它還包括了全域變數和被 const 限定的變數. 我們來看一個例子 :

auto i {42};
int main(int argc, char *argv[]) {

    const auto x {42};

    auto lambda {[]() -> void {

        const auto i1 {i};

        const auto i2 {x};

    }};

    lambda();

}

上述程式碼是可以順利通過編碼的, 因為 main 函式內部的 x 和全域變數 i 都是以靜態的方式存儲. 所以, 它們也可以直接在 Lambda 表達式內部使用而無需捕獲. 因此, 我們曾經提到的 static 變數在 Lambda 表達式內可以直接使用, 其中的 static 變數 其實是泛指以靜態方式存儲的變數

昨天晚上在看 Proposal 的時候看到這個例子, 一開始還覺得比較奇怪, 為什麼沒有捕獲就可以直接使用, 後來才想明白靜態變數是一個泛指. 這個例子以及被忽略的點如果沒有提前說明, 可能會對最後部分的閱讀產生一些困擾, 所以在此提前敘述

Proposal 中以某些條款獲得全票通過的自豪開始, 講述了一個泛型 Lambda 表達式具備的特性

首先, 要想宣告一個泛型 Lambda 表達式, 其參數列表中必須出現 auto 或者樣板參數 :

template <typename T>

auto lambda {[](T t) -> T {

    return t;

}};

int main(int argc, char *argv[]) {

    auto lambda {[](auto param) -> auto {

        return param;

    }};

}

我們可以看到, 在 main 函式內部的 Lambda 表達式的參數列表是一個由 auto 組成的參數列表, 回傳型別也為 auto, 這樣就有了一個泛型的 Lambda 表達式

而在 main 函式外部有一個由樣板參數組成的參數列表, 回傳型別為樣板參數, 這也可以宣告一個泛型的 Lambda 表達式. 但是這種方式宣告的 Lambda 表達式並不能放入函式內部, 因為樣板不能直接出現在函式內部, 所以只能將其放在函式外面. 這種方式是 Proposal 中完全沒有提到的, 由於暫時不了解 C++ 標準, 所以我並不確定這是不是 Clang++ 的補充特性

配合 C++ 14 的自動推斷函式回傳型別, 那麼回傳型別就可以直接省略掉了 :

template <typename T>

auto lambda {[](T t) {

    return t;

}};

int main(int argc, char *argv[]) {

    auto lambda {[](auto param) {

        return param;

    }};

}

C++ 把上述泛型 Lambda 的參數稱為可變參數, 但是一旦在編碼期型別被確定之後, 型別就不能再改變了. 所以這並不改變 C++ 中除去 C-Style 之外, 是一個強型別的程式設計語言這一特性, 這就和 auto 差不多

對於捕獲列表為空的泛型 Lambda 表達式, C++ 支援讓其轉型為適合的指標 :

auto lambda {[](auto param) {

    return param;

}};

int (*p)(int) {lambda};

此時, 不可以再用 auto 推斷 p 的型別, 因為 Lambda 表達式中的回傳型別以及參數型別本身型別就是不明確的, 我們需要明確指出泛型 Lambda 表達式中 auto 的具體型別. 如果僅僅是

auto p {lambda};

那麼 p 只是 lambda 的一個副本而已

若一個泛型 Lambda 表達式中的捕獲列表不為空, 那麼就不能使用指標指向它們, 否則就會產生編碼錯誤

那麼編碼器是如何去處理泛型 Lambda 表達式的呢? 和 Lambda 表達式相同, 編碼器會為其生成一個不具名的類別. 對於

auto lambda {[](const auto &param1, auto param2) {

    return param1 + param2;

}};

這樣的泛型 Lambda 表達式來講, 編碼器會為其生成這樣的不具名類別 :

struct /* anonymous */ {

    template <typename T, typename U>

    auto operator()(const T &param1, U param2) const {

        return param1 + param2;

    }

private:

    template <typename T, typename U>

    static auto __invoke(const T &param1, U param2) {

        return param1 + param2;

    }

    template <typename T, typename U, typename Ret>

    using fptr_t = Ret (*)(const T &, U);

public:

    template <typename T, typename U, typename Ret>

    operator fptr_t<T, U, Ret>() const {

        return &__invoke;

    }

};

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

引入泛型 Lambda 表達式之後, 也就有了更多玩法, 接下來有一個例子, 這是由於引入泛型 Lambda 之後導致的有趣的結果 - 選擇性捕獲 (這裡就用到了最開始所提到的 Lambda 表達式對於靜態存儲的變數可以直接捕獲並且使用) :

#include <type_traits>



using namespace std;

struct X {

    X(int);

    operator int() const;

};

template <bool C>

constexpr typename conditional<C, int, X>::type f(typename 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

}

程式碼中的 conditional 是根據第一個樣板引數確認成員 type 的型別, 當給定的樣板引數為 true, 那麼選擇第一個型別作為 type 的型別; 否則, 選擇第二個型別作為 type 的型別

我們直接分析 g('a'); 此時, 樣板具現化之後, 函式樣板 f 的結果為 :

template <>

constexpr int f(int);

放入的引入為 x, 而 x 是靜態存儲的, Lambda 表達式內部可以直接使用, 這不會導致錯誤

但是當 Lambda 表達式接受的引數為 int 型別或者其它大小和 char 不同的型別的時候, 函式樣板 f 就會有這樣的具現化結果 :

template <>

constexpr X f(const int &);

這個時候, 函式 f 接受一個 int 的參考, 而對於 Lambda 表達式內部來說, 只要是對外部的某個變數進行參考, 不管它是否為靜態存儲的, 都必須先進行捕捉. 而實際上參數列表並沒有對 x 進行捕獲, 所以導致了編碼錯誤

實際上, 可以利用這一特性, 製作一個類似於 static_assert 的功能

 

引入泛型 Lambda 表達式之後, C++ 的泛型又增強了一些. 在函數式的程式設計中, Lambda 表達式佔據了非常高的地位. 這份 Proposal 的目的也是非常明顯, 為 C++ 擴充了更加有用的內容

這裡開個玩笑, C++ 會不會變成 :

auto auto {[auto](auto, auto, ...) {

    return auto;

}};