专业的编程技术博客社区

网站首页 > 博客文章 正文

C++20尝鲜:Lambda表达式变化(c++ lambda表达式原理)

baijin 2024-08-13 00:56:35 博客文章 7 ℃ 0 评论

语言特性

提案

Allow lambda capture [=, this]

P0409R2

Familiar template syntax for generic lambdas

P0428R2

Simplifying implicit lambda capture

P0588R1

Default constructible and assignable stateless lambdas

P0624R2

Lambdas in unevaluated contexts

P0315R4

Allow pack expansion in lambda init-capture

P0780R2

P2095R0

Deprecate implicit capture of this via [=]

P0806R2


lambda捕获 [=, this]

当隐式捕获时(使用[=]),它总是通过引用捕获。为了消除这种混淆,C++20弃用这种行为,允许更明确的[=,this],但[&]保持不变

捕获 是零或更多捕获符的逗号分隔列表,可选地以 默认捕获符 开始。仅有的默认捕获符是

  • &(以引用隐式捕获被使用的自动变量)
  • = (以复制隐式捕获被使用的自动变量)
#include <iostream>
#include <type_traits>

struct LT{
    void f(){
        [=]{
            std::cout << typeid(this).name() << this->val << std::endl;
        }();          //从 C++20弃用, 传引用捕获 this
    }
    void g(){
        [=, *this]{
            std::cout << typeid(this).name() << this->val << std::endl;
        }();   // 从 C++17, 传值捕获 this

    }
    void h(){
        [=, this]{
            std::cout << typeid(this).name() << this->val << std::endl;
        }();    //从 C++20, 传引用捕获 this
    }
    int val = 1;
};

int main()
{
    LT lt;
    lt.f();
    lt.g();
    lt.h();

    return 0;
}
https://wandbox.org/nojs/gcc-head
https://wandbox.org/nojs/clang-head


通用lambda的模板参数列表

C++20允许使用熟悉的模板函数语法直接引入类型。

#include <iostream>
#include <type_traits>
#include <vector>

  // 完美转发
template <typename... T>
void print(T &&... t)
{
    (std::cout << ... << t) << std::endl;
}

int main()
{
    std::vector<int> ivec = {0, 1, 2, 3, 4, 5};
    // lambda 期望 std::vector<T>
    // 在 C++20前
    [](auto vec){
        using T = typename decltype(vec)::value_type;
        for(auto& v : vec) {
            T t = v;
            std::cout << t << std::endl;
        }
         
    }(ivec);
    // 从 C++20后
    []<typename T>(std::vector<T> vec){
        for(auto& v : vec) {
            std::cout << v << std::endl;
        }
    }(ivec);

    // 使用参数类型
    // 在 C++20前
    [](const auto& x){
        using T = std::decay_t<decltype(x)>;
        T copy = x; 
        using Iterator = typename T::const_iterator;
        Iterator iter = x.cbegin();
        std::cout << *iter << std::endl;
    }(ivec);
    //  从 C++20后
    []<typename T>(const T& x){
        T copy = x;
        using Iterator = typename T::const_iterator;
        Iterator iter = x.cbegin();
        std::cout << *iter << std::endl;
    }(ivec);

    // 完美转发
    // 在 C++20前
    [](auto&&... args){
        print(std::forward<decltype(args)>(args)...);
    }(1, 2.2, 'c');
    // 从 C++20后
    []<typename... Ts>(Ts&&... args){
        print(std::forward<Ts>(args)...);
    }(1, 2.2, 'c');

    // 混合auto和T
    []<typename T>(const T& a, auto b){
        std::cout << a << b << std::endl;
    }(1,"hello");

    return 0;
}

在未计算的上下文中

Lambda表达式可以在未计算的上下文中使用,如sizeof()、typeid()、decltype()等。主要的原则是lambdas有一个唯一的未知类型,两个lambdas及其类型永远不相等。

// 以下模板是两个不同的声明
template<class T> void f(decltype([]{}) (*s)[sizeof(T)]);
template<class T> void f(decltype([]{}) (*s)[sizeof(T)]);

在下面的例子中,f()在两个翻译单元中增加同一个计数器,因为内联函数的行为就好像它只有一个定义。然而,g_s违反了ODR,因为尽管它只有一个定义,但仍然有多个不同的声明,因为在a.cpp和b.cpp中有两个不同的lambdas,因此,S有不同的非类型模板参数.

a.h

template<typename T>
int counter(){
    static int value{0};
    return ++value;
}

inline int f(){
    return counter<decltype([]{})>();
}

template<auto> struct S{ int call(){static int value{0};return ++value;} };
// cast lambda to pointer
inline S<+[]{}> g_s;

b.cpp

#include <iostream>
#include "a.h"

void func(){
    auto v = f();
    std::cout << "f:" << v << std::endl;
    int gv = g_s.call();
    std::cout << "g:" << gv << std::endl;
}

a.cpp

#include <iostream>
#include "a.h"

void func();
int main()
{
    auto v = f();
    std::cout << "f:" << v << std::endl;
    int gv = g_s.call();
    std::cout << "g:" << gv << std::endl;
    func();

    return 0;
}
https://wandbox.org/permlink/0nIwKlQa3hsRp2LJ

运行结果:

f:1
g:1
f:2
g:1

默认的可构造和可赋值的无状态lambda

在C++20中,无状态lambda是默认可构造和可赋值的,在未求值上下文中,我们可以通过decltype()获得lambda的类型,并在稍后创建该类型的变量。

例子中,std::map接受一个比较器类型,以便稍后实例化它。虽然我们可以在C++17中获得一个lambda类型,但无法实例化它,因为lambdas不是默认可构造的。

#include <iostream>
#include <type_traits>
#include <map>

int main()
{
    auto greater = [](auto x,auto y){
        std::cout << x << y << std::endl;
        return x > y;
    };
    // 需要是默认可构造的类型
    std::map<std::string, int, decltype(greater)> map1;
    // 需要是默认可赋值的类型
    auto map2 = map1;  
    map2["1"] = 1;
    map2["2"] = 2;   
    map2["3"] = 3;  
    return 0;
}

运行结果:

12
21
21
13
23
32
32

在lambda捕获中进行参数包展开

C++20简化了lambda中的参数包捕获。在C++20之前,如果我们想移动包,可以通过值、引用或std::tuple来捕获它们。现在就简单多了,我们可以在初始化捕获中直接捕获参数包。它并不局限于std::move或std::forward,任何函数都可以应用于参数包元素。

#include <iostream>
#include <type_traits>
#include <map>

void f(double, double){std::cout << "fd" << std::endl;}
void g(int, int,double){std::cout << "gi" << std::endl;}

// C++17
template<class F, class... Args>
auto delay_apply(F&& f, Args&&... args) {
    return [f=std::forward<F>(f), tup=std::make_tuple(std::forward<Args>(args)...)]()
            -> decltype(auto) {
        return std::apply(f, tup);
    };
}

// C++20
template<typename F, typename... Args>
auto delay_call(F&& f, Args&&... args) {
    return [f = std::forward<F>(f), ...f_args=std::forward<Args>(args)]()
            -> decltype(auto) {
        return f(f_args...);
    };
}

int main()
{
    delay_apply(f, 1.1, 2.2)();
    delay_call(g, 1, 2, 3.3)();
    return 0;
}

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表