这是合法的模板 lambda 语法吗?

Is this legal template lambda syntax?

在重构一些遗留代码时,我遇到了这种在 STL 算法中使用的谓词的传统实现:

template<bool b>
struct StructPred {
    bool operator()(S const & s) { return s.b == b; }
};

累到鲍尔默峰了,不小心改写成了这样的lambda,看起来很自然,也很管用:

template<bool b>
auto lambda_pred = [] (S const & s) { return s.b == b; };

后来我意识到我从来没有见过这样的模板lambda。我在 cppreference 或 Whosebug 上找不到任何类似的东西。生成模板 lambda 的规范方法似乎是将它们包装在模板结构或模板函数中。 C++20 为 lambda 引入了命名模板参数,但这是一种不同的语法(在捕获括号之后)。

现在我的问题是:这是合法的语法吗?它记录在任何地方吗?它甚至是 lambda 还是其他什么?与包装替代品相比,是否有任何影响或副作用?为什么每个人都推荐包装器实现?我是否漏掉了一些明显的东西?

下面和 godbolt 处的完整工作测试代码。只是为了确保我还添加了一个类型模板参数版本。 MSVC、GCC 和 clang 对这段代码很满意。

#include <vector>
#include <algorithm>

struct S {
    bool b = false;
};

// classic function object
template<bool b>
struct StructPred {
    bool operator()(S const & s) { return s.b == b; }
};

// template function producing a lambda
template<bool b>
auto make_pred() {
    return [] (S const & s) { return s.b == b; };
}

// direct template lambda
template<bool b>
auto lambda_pred = [] (S const & s) { return s.b == b; };

// also with type params
template<typename T, bool b>
auto lambda_pred_t = [] (T const & t) { return t.b == b; };

std::pair<size_t, size_t> count1(std::vector<S> const & v) {
    return {
        std::count_if(v.begin(), v.end(), StructPred<true>{}),
        std::count_if(v.begin(), v.end(), StructPred<false>{})
    };
}

std::pair<size_t, size_t> count2(std::vector<S> const & v) {
    return {
        std::count_if(v.begin(), v.end(), make_pred<true>()),
        std::count_if(v.begin(), v.end(), make_pred<false>())
    };
}

std::pair<size_t, size_t> count3(std::vector<S> const & v) {
    return {
        std::count_if(v.begin(), v.end(), lambda_pred<true>),
        std::count_if(v.begin(), v.end(), lambda_pred<false>)
    };
}

std::pair<size_t, size_t> count4(std::vector<S> const & v) {
    return {
        std::count_if(v.begin(), v.end(), lambda_pred_t<S, true>),
        std::count_if(v.begin(), v.end(), lambda_pred_t<S, false>)
    };
}

void test() {
    std::vector<S> v{3};
    v[1].b = true;
    // all implementations correctly return {1,2}
    auto c1 = count1(v);
    auto c2 = count2(v);
    auto c3 = count3(v);
    auto c4 = count4(v);
}
template<bool b>
auto lambda_pred = [] (S const & s) { return s.b == b; };

这实际上不是分配给 lambda 的 template-lambda, it is rather a variable template

它不等同于将模板参数添加到隐式声明的闭包 struct 中,该闭包将此 lambda 作为调用运算符(传统方法):

template<bool b>
struct StructPred { // NOT equivalent to this
    bool operator()(S const & s) { return s.b == b; }
};

struct StructPred { // NOT equivalent to this either
    template<bool b>
    bool operator()(S const & s) { return s.b == b; }
};

这相当于根据变量的模板参数创建不同的闭包。因此,对于 bool 示例,这就像在以下类型之一的 operator() 之间进行选择:

struct StructPred_true {
    bool operator()(S const & s) { return s.b == true; }
}

struct StructPred_false {
    bool operator()(S const & s) { return s.b == false; }
}

此方法不允许部分专业化,因此功能较弱。 这种方法可能不受欢迎的另一个原因是它不能让您轻松访问闭包类型。 StructPred 可以显式使用,不像匿名的 类 StructPred_trueStructPred_false

C++20 中的模板 lambda 如下所示:

auto lambda = []<bool b>(S const & s){ return s.b == b; };

这相当于使闭包的 operator() 模板化。

以下所有标准参考均指N4659: March 2017 post-Kona working draft/C++17 DIS


通用 lambda:C++14 的一个特性

The canonical way of producing template lambdas seems to be wrapping them in template structs or template functions. C++20 introduces named template params for lambdas, but that's a different syntax (after the capture brackets).

另一个答案彻底解释了 OPs 变量模板的构造是什么,而这个答案解决了上面 emphasized 的部分;也就是说,从 C++14 开始,通用 lambdas 是一种语言特性,而不是仅从 C++20 开始可用的东西。

根据 [expr.prim.lambda.closure]/3 [摘录]:

[...] For a generic lambda, the closure type has a public inline function call operator member template whose template-parameter-list consists of one invented type template-parameter for each occurrence of auto in the lambda's parameter-declaration-clause, in order of appearance. [...]

通用 lambda 可以声明为

auto glambda = [](auto a, auto b) { return a < b; };

相当于

struct anon_struct {
    template<typename T, typename U>
    bool operator()(T a, U b) { return a < b; }
}

而不是

template<typename T, typename U>
struct anon_struct {
    bool operator()(T a, U b) { return a < b; }
}

作为单个通用 lambda 对象(其 闭包类型 实际上不是 class 模板而是 non-template(non-union) class) 可用于为其发明的模板参数的不同实例化一般地调用其函数调用运算符模板。

#include <iostream>
#include <ios>

int main() {
    auto gl = [](auto a, auto b) { return a < b; };
    std::cout << std::boolalpha 
        << gl(1, 2) << " "      // true ("<int, int>")
        << gl(3.4, 2.2) << " "  // false ("<double, double>")
        << gl(98, 'a');         // false ("<int, char>")
}

具有显式模板参数列表的通用 lambda:C++20 特性

从 C++20 开始,我们可以在 声明 泛型 lambda 时使用显式模板参数列表,以及在 调用 通用 lambdas。

在 C++14 和 C++17 中,通用 lambda 的模板参数只能隐式声明为发明的 type 每个声明的模板参数 auto lambda 声明中的参数,具有以下限制:

  • 发明的模板参数只能是类型模板参数合成(如上图),
  • 无法在 lambda 主体中直接访问类型模板参数,但需要在相应的 auto 参数上使用 decltype 提取。

或者,如人为示例所示:

#include <type_traits>

// C++17 (C++14 if we remove constexpr
//        and use of _v alias template).
auto constexpr cpp17_glambda = 
    // Template parameters cannot be declared
    // explicitly, meaning only type template
    // parameters can be used.
    [](auto a, auto b) 
        // Inventend type template parameters cannot
        // be accessed/used directly.
        -> std::enable_if_t<
             std::is_base_of_v<decltype(a), decltype(b)>> {};

struct Base {};
struct Derived : public Base {};
struct NonDerived {};
struct ConvertsToDerived { operator Derived() { return {}; } };
    
int main() {
    cpp17_glambda(Base{}, Derived{});    // Ok.
    //cpp17_glambda(Base{}, NonDerived{}); // Error.
    
    // Error: second invented type template parameter
    //        inferred to 'ConvertsToDerived'.
    //cpp17_glambda(Base{}, ConvertsToDerived{});
    
    // OK: explicitly specify the types of the invented
    //     type template parameters.
    cpp17_glambda.operator()<Base, Derived>(
        Base{}, ConvertsToDerived{});
}

现在,在 C++20 中,引入了 lambda 的名称模板参数(以及 requires 子句),上面的示例可以简化为:

#include <type_traits>

auto constexpr cpp20_glambda = 
    []<typename T, typename U>(T, U) 
        requires std::is_base_of_v<T, U> { };

struct Base {};
struct Derived : public Base {};
struct NonDerived {};
struct ConvertsToDerived { operator Derived() { return {}; } };

int main() {
    cpp20_glambda(Base{}, Derived{});    // Ok.
    //cpp20_glambda(Base{}, NonDerived{}); // Error.
    
    // Error: second type template parameter
    //        inferred to 'ConvertsToDerived'.
    //cpp20_glambda(Base{}, ConvertsToDerived{});
    
    // OK: explicitly specify the types of the
    //     type template parameters.
    cpp20_glambda.operator()<Base, Derived>(
        Base{}, ConvertsToDerived{});
}

而且我们还可以使用不一定是类型模板参数的模板参数声明 lambda:

#include <iostream>
#include <ios>

template<typename T>
struct is_bool_trait {
    static constexpr bool value = false;  
};

template<>
struct is_bool_trait<bool> {
    static constexpr bool value = true;  
};

template<typename T>
struct always_true_trait {
    static constexpr bool value = true;    
};

int main() {
    auto lambda = []<
        template<typename> class TT = is_bool_trait>(auto a) -> bool { 
        if constexpr (!TT<decltype(a)>::value) {
            return true;  // default for non-bool. 
        }
        return a; 
    };
    std::cout << std::boolalpha 
        << lambda(false) << " "                            // false
        << lambda(true) << " "                             // true
        << lambda(0) << " "                                // true
        << lambda(1) << " "                                // true
        << lambda.operator()<always_true_trait>(0) << " "  // false
        << lambda.operator()<always_true_trait>(1);        // true
}