【问题标题】:Is this legal template lambda syntax?这是合法的模板 lambda 语法吗?
【发布时间】:2020-12-21 07:56:11
【问题描述】:

在重构一些遗留代码时,我遇到了这种用于 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 或 stackoverflow 上找不到类似的东西。生成模板 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);
}

【问题讨论】:

    标签: c++ templates lambda c++14


    【解决方案1】:
    template<bool b>
    auto lambda_pred = [] (S const & s) { return s.b == b; };
    

    这并不是真正的 template-lambda,而是分配给 lambda 的 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; }
    }
    

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

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

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

    这相当于将 Closure 的 operator() 模板化。

    【讨论】:

    • 据我了解,那些 C++20 模板参数列表只是使我们能够为 operator()(auto...) 的其他发明参数命名。我认为不能在该列表中指定非类型参数,或者任何其他不是 operator() 参数的类型参数。你会如何用真假来称呼这样的事情,或者像我的例子一样将它作为谓词传递?
    • 好的,我找到了一种直接调用C++20 lambda的方法,但是你自己看看::) bool x = lambda_pred_20.operator()(v[0]);
    • @Simpleton 请注意,您可以对 C++20 之前的通用 lambdas 使用相同的方法,从调用站点显式指定发明的 type 模板参数的类型,从而覆盖这些模板参数的模板参数推导。但是,C++20 方法提供的,除了符合(与其他通用实体)之外,还使用类型以及非类型模板参数,这些模板参数不能通过模板参数推导推导出来,而且可以设置为默认值,@ 987654323@.
    • 然而它等同于template&lt;bool b&gt; struct { bool operator()(S const &amp; s) { return s.b == b; } } struct_pred;
    【解决方案2】:

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


    通用 lambda:C++14 特性

    生成模板 lambdas 的规范方法似乎是将它们包装在模板结构或模板函数中。 C++20 为 lambda 引入了命名模板参数,但这是一种不同的语法(在捕获括号之后)。

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

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

    [...] 对于通用 lambda,闭包类型有一个公共内联函数调用运算符成员模板,其模板参数列表由一个发明的类型模板参数组成,用于 lambda 参数中每次出现的auto -declaration-clause,按出现顺序。 [...]

    一个通用的 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 对象(其 闭包类型 实际上不是类模板而是非模板(非联合)类)是必不可少的,可用于通用调用其函数为其发明的模板参数的不同实例调用运算符模板。

    #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 时使用显式模板参数列表,并在调用时提供用于提供显式模板参数的糖化语法> 通用 lambda。

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

    • 发明的模板参数只能是type合成的模板参数(如上图),并且
    • 类型模板参数不能在 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
    }
    

    【讨论】:

    • 赞成对泛型 lambda 的精彩解释。但是,我并不是说它们是 C++20 的特性,我只是提到了 C++20 引入的 named 参数列表。我的问题是关于为 lambdas 实现“编译时绑定”的不同方法,可以这么说。
    猜你喜欢
    • 2021-06-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-02-11
    • 2021-02-04
    • 2014-08-18
    相关资源
    最近更新 更多