C++ Lambda

第五章 Lambda in C++20

预计阅读时间6 分钟 18 views

Lambda 表达式在 C++ 20 中的变化:

  • 捕获 'this' 指针的新选项。
  • 模板 Lambdas。
  • 使用概念来改进泛型 Lambdas。
  • 如何在 'constexpr' 算法中使用 Lambdas。
  • 如何进一步简化重载模式。

Lambda 表达式语法更新

在 C++20 中, Lambda 表达式的语法发生了一些变化:

  • 现在可以在参数列表后添加 'consteval'。
  • 可以选择指定模板尾部。
  • 在尾返回类型之后,还可以添加 'requires' 声明。
  • 更新后的语法图:
  • the lambda introducer with an optional capture list | | template parameter list(optional) | | trailing return type | | | v v v []<tparams> () specifiters exception attr -> ret { /* code */ } ^ ^ ^ | | | | | lambda body | | | mutable, constexpr, consteval, noexcept, attributes(optional) | parameter list (optional when no specifiers added)

C++20 中的 Lambda 表达式更变一览

在 C++20 中,Lambda 有如下的新特性:

  • 允许 '[=, this]' 作为 Lambda捕获方式,并弃用通过 '[=]' 隐式捕获 'this' 的方式(P0409R2P0806)。
  • 支持在 Lambda 初始化捕获中进行包扩展,如 '...args = std::move(args)](){}'(P0780)。
  • 支持 'static','thread_local' 变量和结构化绑定的 Lambda 捕获(P1091)。
  • 模板 Lambda(也支持概念约束)(P0428R2)。
  • 简化隐式 Lambda 捕获(P05881R1)。
  • 支持默认构造和赋值的无状态 Lambda(P0624R2)。
  • 支持在未求值上下文中的 Lambda(P0315R4)。
  • 'constexpr' 算法的改进,尤其是P0202、 P0879P1645

示例:在 Lambda 表达式中的结构化绑定

#include <tuple>
#include <string>

auto GetParam(){
    return std::tuple { std::string{"Hello world"}, 42};
}

int main(){
    auto[x, y] = GetParams();
    const auto ParamLength = [&x, &y]() { return x.length() + y;}();
    return ParamLength;
}
C++

C++20 对 'this' 捕获进行了限制,如果在方法中使用 '[=]' 进行捕获:

struct Baz{
    auto foo(){
        return [=] { std::cout << s << '\n'; };
    }
    std::string s;
};
C++

在 CLang 中会有如下警告:

Implicit capture of 'this' with a capture default of '=' is deprecated
C++

警告的原因是,即使使用 '[=]',最终仍会以指针形式捕获 'this'。较好的做法是明确指出捕获的方式,例如 '[=, this]' 或 '[=,*this]' 。

consteval Lambdas

在 C++11 中引入的 'constexpr' 允许函数在编译阶段执行,但这些函数也可以在运行时执行,然而,在某些情况下,可能需要手动将功能限制在仅编译时执行。 因此,C++20 引入了一个新的关键字 'consteval',用于创建只能在编译时求值的函数,这类函数也被称为"即时函数"。

一个使用 consteval 函数示例:

int main(){
    const int x = 10;
    auto lam = [](int x) consteval { return x + x; };
    return lam(x);
}
C++

解释:在上述代码中,在 Lambda 参数列表后应用了 'consteval',这与使用 'constexpr' 非常相似, 其区别在于,如果移除 'x' 的 'const' 修饰符,'constexpr' Lambda 仍然可以在运行时工作,而即使的 Lambda 表达式则无法通过编译。

默认情况下,如果 Lambda 函数体符合 'constexpr' 函数的规则,编译器会隐式地将调用操作符标记为 'constexpr'。然而,'consteval' 施加了更严格地规则——不能同时使用这两个关键字。

捕获参数包

使用 Lambda 表达式捕获参数包

template<typename... Args>
void call(Args... args){
    auto ret = [...capturedArgs = std::move(args)](){};
}
C++

在 C++20 之前,这段代码是无法通过编译的,如果要绕过这个问题,在之前必须将参数包包装到一个单独的元组中。

利用折叠表达式来输出每个捕获的对象:

#include <iostream>
#include <memory>

template<class First, class... Args>
void captureTest(First&& first, Args&&... args){
    const auto printer = [first = std::move(first), ...capturedArgs = std::move(args)]{
        std::cout << first;
        ((std::cout << ", " << capturedArgs), ...);
        std::cout << '\n';
    };
    printer();
}

int main(){
    auto ptr = std::make_unique<int>(10);
    captureTest(std::move(ptr), 2, 3, 4);
    captureTest(std::move(ptr), 'a', 'b');
}
/* output:
 * 0xbd23b923d0, 2, 3, 4
 * 0, a, b
 */
C++
  • 解释:上述代码中,使用一个 'printer' 对象来捕获对象,而不是将其作为 Lambda 参数转发,同时,声明了一个 'unique_ptr',当其被传递给 Lambda 两次时,第二次输出的指针值为 0, 这是因为指针已经失去了对内存块的所有权。

模板 Lambda 表达式

在 C++14 中引入了通用 Lambda 表达式,这意味用 'auto' 声明的参数实际上是模板参数:

[](auto x) { x; }
C++

对于上述 Lambda,编译器生成了一个对应于以下模板方法的调用操作符:

template<typename T>
void operator()(T x) { x; }
C++

然而,在 C++14 中,无法更改此模板参数,也无法使用"真正的"模板参数,在 C++20 中,这点得到了改善,使得可以使用模板参数定义 Lambda。

限制 Lambda 的参数类型

如果要将 Lambda 限制为只接受某种类型的 'vector',可以编写一个通用的 Lambda:

auto foo =[](auto& vec){
    std::cout << std::size(vec) << '\n';
    std::cout << vec.capacity() << '\n';
}
C++

但如果使用 'int' 参数调用它(例如 'foo(10);'),可能会因为没有合适实参类型的函数进行调用。

在 C++20 中可以使用模板参数明确指出调用何时函数:

auto foo = []<typename T>(std::vector<T> const& vec){
    std::cout << std::size(vec) << '\n';
    std::cout << vec.capacity() << '\n';
};
C++

上述 Lambda 表达式会被解析为一个带有模板参数的调用操作符:

template<typename T>
void operator()(std::vector<T> const& vec) { ... }
C++

模板参数位于捕获列表 '[]' 之后,如果使用 'int' 类型调用它('foo(10);'),会显示的错误如下:

note: mismatched types 'const std::vector<T>' and 'int'
C++

在泛型 Lambda 中,只有一个变量而没有它的模板类型,如果要访问这个类型,就需要使用 'decltype(x)'(针对参数为 'auto x' 的 Lambda 表达式),这会使得代码变得冗长且复杂:

auto f = [](auto const& x){
    using T = std::decay_t<decltype(x)>;
    T copy = x;
    T::static_function();
    using Iterator = typename T::iterator;
}
C++

在 C++20 中,可以这样写:

auto f = []<typename T>(T const& x){
    /* leave out 'delctype' */
    T copy = x;
    T::static_function();
    using Iterator = typename T::iterator;
}
C++

此时,如下代码:

[](auto x) { x; }
C++

通过这种方式来获取参数的类型,就需要使用 'decltype':

using T = std::decay_t<decltype(x)>;
C++

但在 C++20 中,可以直接访问模板参数。

完美转发在泛型可变参数的 Lambda 表达式中的应用: 在 C++17 中:

auto ForwardToTestFunc = [](auto&& ...args){
    return TestFunc(std::forward<decltype(args)>args...);
}
C++

如果使用上面这种方法访问模板参数的类型时,都需要使用 'decltype()',而在 C++20 中,通过模板 Lambda 就可以省去这一步骤:

auto ForwardToTestFunc = []<typename... T>(T&&... args){
    return TestFunc(std::forward<T>(args)...);
};
C++

概念(concept) 与 Lambda 表达式

概念(concepts)是一种革新的模板编写方式,它允许为模板参数设置约束,从而提高代码的可读性,加快编译速度,并提示出更明确的错误信息:

// 定义一个概念:
template<class T>
concept SignedIntegral = std::is_integral_v<T> && std::is_signed_v<T>;

// 使用概念:
template<SignedIntegral T>
void signedIntsOnly(T val) { }
C++
  • 解释:在上述代码中,创建了一个描述"有符号且是整数类型"的概念(此处需要使用到类型特性 type traits),然后,利用这个概念来定义一个模板函数,该函数仅支持与该概念匹配的类型。此处没有直接使用 'typename T',而是直接引用了概念的名称。

与 Lambda 表达式的关系

关键在于这种简洁的语法以及对 'auto' 模板参数的约束。

简化与简化语法

由于概念的简洁语法,使得在编写模板时省略 'template<typename...>' 部分:

void myTemplateFunc(auto param) { }
C++

或者使用约束的 'auto':

void signedIntsOnly(SignedIntegral auto val) { }
void floatsOnly(std::floating_point auto fp) { }
C++

这种语法类似于在 C++14 中的泛型 Lambda:

void myTemplateFunction(auto val){ }
C++

对于 Lambda 表达式,可以利用这种简洁的风格,并对泛型 Lambda 参数施加额外的限制:

auto genLambda = [](SignedIntegral auto param) { return param * param + 1; };
C++

此处,使用 'SignedIntegral'概念约束了'auto param',使整个表达式相较于之前的 Lambda 更具有可读性。

更复杂的示例

定义一个类接口的概念:

// IRenderable 概念,使用 requires 关键字
template<typename T>
concetp IRenderable = requires(T v){
    {v.render()}-> std::same_as<void>;
    {v.getVertCount()}-> std::convertible_to<size_t>;
};
C++

在上面的代码中,定义了一个匹配所有具有 'render()' 和 'getVertCount()'成员函数的类型概念,然后再用其实现一个泛型 Lambda:

#include <iostream>
#include <concepts>

template<typename T>
concept IRenderable = requires(T v){
    {v.render()}-> std::same_as<void>;
    {v.getVertCount()}-> std::convertible_to<size_t>;
};

struct Circle{
    void render() { std::cout << "drawing circle\n"; }
    size_t getVertCount() const { return 10; };
};

struct Square{
  void render() { std::cout << "drawing square\n"; }
  size_t getVertCount() const { return 4; };
}

int main(){
    const auto RenderCaller = [](IRenderable auto& obj){
        obj.render();
    };

    Circle c;
    RenderCaller(c);

    Square s;
    RenderCaller(s);
}
C++

无状态的 Lambdas 的变化

在 C++11 中,即使是无状态的(stateless)的 Lambda 表达式也不是默认可构造的,然而,这一限制在 C++20 中被解除:

#include <set>
#include <string>
#include <iostream>

struct Product{
    std::string name;
    int id { 0 };
    double price { 0.0; }
};

int main(){
    const auto nameCmp = [](const auto& a, const auto& b){ return a.name < b.name; };
    const std::set<Product, decltype(nameCmp)> prodSet{
        {"Cup", 10, 100.0}, {"Book", 2, 200.5},
        {"TV set", 1, 2000}, {"Pencil", 4, 10.5}
    };
    for(const auto& elem: prodSet) std::cout << elem.name << '\n';
}
C++
  • 解释:在上述代码中,使用一个 'set' 来存储 'Product' 列表,为了比较产品,传递了一个无状态的 Lambda 表达式来比较他们的字符串名称。

如果使用 C++17 来编译上述代码,会出现一个 '默认构造器已经被删除' 的错误:

note: a lambda closure type has a deleted default constructor
  824 |     const auto nameCmp = [](const auto& a, const auto& b){ return a.name < b.name; };
note:   when instantiating default argument for call to 'std::set<_Key, _Compare, _Alloc>::set(std::initializer_list<_Tp>, const _Compare&, const allocator_type&) [with _Key = Product; _Compare = const main()::<lambda(const auto:1&, const auto:2&)>; _Alloc = std::allocator<Product>; allocator_type = std::allocator<Product>]'
  828 |     };
C++

C++20 中的改进

在 C++20 中,可以存储无状态的 Lambda,甚至可以复制他们:

template<typename F>
struct Product{
    int id{ 0 };
    double price { 0.0 };
    F predicate;
};

int main(){
    const auto idCmp = [](const auto& a) noexcept{
        return a.id != 0;
    };

    Product p {10, 10.0, idCmp};
    [[maybe_unused]] auto p2 = p;
}
C++
  • 解释:在上述代码中,定义了一个 'Product' 的结构体,该结构体包含一个 Lambda 表达式作为谓词(predicate)。在 C++20 中,这种 Lambda 可以无状态地存储并被复制,而不会导致任何错误。

更高级地未求值上下文(Unevaluated Contexts)

C++20 中还引入了一些与高级用例相关的更改,例如未求值上下文。结合无状态 Lambda 的默认可构造性,可以进行如下操作:

std::map<int, int, decltype([](int x, int y){ return x > y; }) > map;
C++

在上述代码中,可以在 'map' 容器中声明直接指定 Lambda 表达式,并将其用作比较器的可调用类型。

Lambda 表达式和 'constexpr' 算法

在 C++20 中,大部分标准算法都被标记为 'constexpr',这使得 'constexpr' Lambdas 更加方便。

示例1:使用 'std::accumulate' 与自定义 'constexpr' Lambda

#include <array>
#include <numeric>

int main(){
    constexpr std::array arr{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    static_assert(std::accumulate(std::begin(arr), std::end(arr), 0,
        [](auto a, auto b) noexcept{
            return a + b;
        }) == 5);
    return arr[0];
}
C++
  • 解释:上述代码中的 'constexpr' Lambda 使得代码在编译时就能完成计算,从而提高了运行时性能。

示例2:将 'constexpr' Lambda 传递给自定义函数

#include <array>
#include <algorithm>

constexpr auto CountValues(auto container, auto cmp){
    return std::count_if(std::begin(container), std::end(container), cmp);
}

int main(){
    constexpr auto minVal = CountValues(std::array{-10, 6, 8, 4, -5, 2, 4, 6},
        [](auto a) { return a >= 0; });

    return minVal;
}
C++
  • 解释:在上面的代码中,定义了一个 'constexpr' 函数 'CountValues',它接受一个比较器或谓词用于 'count_if'算法,这个函数可以在编译器完成计算。

C++20 中重载模式(Overloaded Pattern)的更新

多个 Lambda 表达式派生并通过重载模式将它们暴露出来,在 C++20 中这种操作方式得到了简化。

由于 C++20 中的类模板参数推导(CTAD)更新,现在可以使用更简洁的语法。这是因为 C++20 对 CTAD 进行了扩展,并且自动处理聚合类型(aggregates),这意味着这不再需要编写自定义的推导规则。 定义一个简单的类型:

template<typename T, typename U, typename V>
struct Triple{
    T t;
    U u;
    V v;
};
C++

在 C++20 中,可以直接写:

Triple ttt{10.0f, 90, std::string{"hello"}};
C++

其中,编译器会自动推导 'T' 为 'float' ,'U' 为 'int','V'为'std::string'。

C++20 中的重载模式

在 C++20 中,可以简化重载模式为:

template<class... Ts>
struct overload: Ts...{
    using Ts::operator()...;
};
C++
  • 解释:上述代码使用了 C++20 的新特性,使得从多个 Lambda 表达式派生并将他们组合在一起变得更加简单。

Leave a Comment

Share this Doc

第五章 Lambda in C++20

Or copy link

CONTENTS
It's late! Remember to rest.