这个法律模板的lambda语法是否合法?

17

在重构一些传统代码时,我发现了这个用于STL算法中的谓词的传统实现:

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

我感到疲惫并且达到了Ballmer峰值,所以我无意中将它重写为一个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);
}
2个回答

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

这并不是一个真正的模板 lambda,而是一个被分配给 lambda 的 变量模板

这与向隐式声明的 Closure 结构体添加模板参数(传统方法)并将其作为调用运算符的 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_true和StructPred_false不同,可以明确使用StructPred

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

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

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


1
据我理解,C++20模板参数列表只是让我们能够为operator()(auto...)中的虚拟参数命名。我认为在该列表中不能指定非类型参数或任何其他不是operator()参数的类型参数。如果要使用true或false来调用此类内容,或者将其作为谓词传递,例如我的示例,应该如何处理? - Simpleton
1
好的,我找到了一种直接调用C++20 lambda的方法,但您自己看看吧::) bool x = lambda_pred_20.operator()<true>(v[0]); - Simpleton
2
@Simpleton 请注意,您可以在C++20之前使用相同的方法来处理通用lambda表达式,从调用站点显式指定发明的_type_模板参数的类型,从而覆盖那些模板参数的模板参数推导。然而,C++20方法提供的不仅是符合性(与其他通用实体),还使用无法通过模板参数推导推断的类型和非类型模板参数,并且可以将其设置为默认值,允许例如(可以说是无用的)代码 - dfrib
2
然而,它等同于 template<bool b> struct { bool operator()(S const & s) { return s.b == b; } } struct_pred; - Caleth

6

下面提到的所有标准引用均指向 N4659:2017年3月后Kona工作草案/C++17 DIS


通用lambda:C++14特性

在C++14中,生成模板lambda的一种方法似乎是将其包装在模板结构或模板函数中。C++20引入了命名的 Lambda 模板参数,但这是一个不同的语法(放置于捕获括号之后)。

另一个回答已经全面解释了 OPs 变量模板是什么构造,而此回答则解释上述部分的重点:即通用 lambda 是 C++14 中引入的语言特性,而不是仅在 C++20 可用的功能。

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

[...] 对于通用的 lambda,闭包类型具有公共的内联函数调用运算符成员模板,该模板参数列表由每个 auto 出现的发明类型模板参数组成,按照出现顺序分别排列。[...]

通用 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>")
}

C++20特性:使用显式的模板参数列表声明泛型lambda表达式

从C++20开始,我们可以在声明泛型lambda表达式时使用显式的模板参数列表,同时还提供了一种简洁的语法来在调用泛型lambda表达式时提供显式的模板参数。

在C++14和C++17中,泛型lambda表达式的模板参数只能隐式地通过每个声明的auto参数来定义为虚构的类型模板参数。限制如下:

  • 虚构的类型模板参数只能被合成(如上所示),
  • 类型模板参数不能在lambda函数体内直接访问,但需要使用decltype关键字提取相应的auto参数。

或者,可以通过一个牵强的例子来说明:

#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引入的命名参数列表。我的问题是关于实现lambda的“编译时绑定”的不同方式,可以这么说。 - Simpleton

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接