c++/cpp lambda表达式的使用

前景

在coding过程中对于共用的代码块进行封装以达成复用已成为我们的习惯。不过复用的过程情况多变,对于某一个多次使用的代码块来说,当我们知道在本文件外甚至本函数外基本不会调用时,往往并不希望为这个代码块麻烦地做单独声明“污染环境”,此时我们可以采用lambda表达式完成复用目的。
此外,在使用cpp某些特性时,使用lambda表达式显然更加方便 比如作为deleter参数传入智能指针时。

前提

编译器需要支持c++11及以上。

什么是lambda表达式

lambda表达式是定义匿名函数对象的便捷方法,通常,lambda用于封装传递到函数的几行代码.

lambda表达式的组成

[=]/*1*/ ()/*2*/ mutable/*3*/ throw()/*4*/ -> int/*5*/ {}/*6*/
    1. capture子句
    1. 参数列表(optional)
    1. 可变规范(optional)
    1. 异常定义(optional)
    1. 返回类型(optional)
    1. 函数体

capture子句

首先我们可以认为lambda表达式定义了一个函数变量,在其语法语义上作为变量处理.
capture子句被lambda用于访问所在作用域的变量, 并指定访问这些变量时是通过值拷贝还是引用访问.
**ps, 在c++14及以上的规范中,capture子句还可以用于定义新变量,本文以c++11标准为重点, 关于此细节感兴趣可以上网查阅)

捕获类型

在[]中使用&时代表引用捕获(ex. [&factor]),此时lambda对于捕获到的变量修改在lambda外部同样生效,当不使用&而直接使用变量名时代表按值捕获(ex.[factor]),此时对该变量的修改尽在lambda函数体中生效.
tips:

  • [&]表示捕获的所有变量都是按照引用捕获的,lambda将会捕获所有在函数体中出现的外部变量.
  • [=]表示捕获的所有变量都是按照值捕获的
  • 可以在capture子句中指定多种捕获模式,例如
[&total, factor]//值捕获factor,引用捕获total
[factor, &total]//值捕获factor,引用捕获total
[&, factor]//值捕获factor,其他变量引用捕获
[=, &factor]//引用捕获factor,其他变量值捕获
  • []表示不访问外部变量
  • 当设定了捕获方式默认值时,不能再单独为其他变量设置相同的捕获方式,此外,不能重复使用标识符.
[=, factor] //error
[&, &factor] //error
[i,i]//error
[&i, i]//error

当需要访问类成员时,对于this的捕获比较特殊,当使用&方式时,捕获方式是&(this),即copy了this指针所指向的地址,当使用=捕获时,捕获方式仍然是&(this),当想要引用this所指向的对象而非其对象地址时应使用*this(c++17引入),这个东西非常恶心,c++随着14 17 20标准的更新改来改去,推荐直接使用&方式捕获this.详情可参考传送门.另外lambda捕获this存在陷阱,详见后文.

参数列表

lambda表达式实质上定义了一个函数对象,所以自然包含参数列表,参数列表的使用符合函数标准规范,我们按照普通函数语法编写即可.

可变规范

lambda表达式默认其通过值捕获的变量都是const类型,即不可更改.当使用mutable关键字时可取消该特性.但是对变量的更改仅在lambda表达式函数体内生效.

异常定义

该语句用于声明函数是否抛出异常,语法与普通函数相同。比如可以使用noexcept 表示不抛出异常。

返回类型

首先lambda表达式会根据函数体中的return 类型推测返回类型,所以返回类型的声明并非必要。不过有时并不能推测类型,比如 return {1, 2},从一个初始化列表推测类型显然不可能,此时你可以选择为lambda表达式指定返回类型。返回类型前必须带有“->"符号. lambda可以返回void类型。

函数体

lambda函数体和普通函数的函数体没有本质区别,只不过lambda可以使用捕获变量:

  • 作用域内的捕获变量
  • 当在类成员函数内使用lambda时,可以捕获到类的this指针,并以此使用类成员和成员函数。
    ex:显式值捕获n,隐式引用捕获m
#include <iostream>
int main()
{
    int m = 0;
    int n = 0;
    [&, n](int a) mutable {m = ++n + a;}(4);
    std::cout << m << std::endl << n << std::endl;
    return 0;
}
5
0

lambda表达式使用陷阱

lambda的引用捕获模式和隐式捕获会使我们在开发时忘记捕获变量的生命周期.

  • 在使用[&]时,我们在lambda函数体内使用的引用可能在其生效的作用域已经销毁.
    ex:
    //main.cpp
int main()
{
std::vector<std::function<bool(int)>> filters;//过滤函数
//添加一个过滤函数,过滤掉
filters.emplace_back([](int value {return value % 5 ==0;});
if(filters[0](18))
{
    std::cout<< "can be diveded by 5 without remain" << std::endl;
}
return 0;
}
//此时没有什么问题,但是当我们写一个函数自动填充过滤器时
//main.cpp
std::vector<std::function<bool(int)>> filters;//过滤函数
void add_filter()
{
    int arr[3] = {1,2,3};
    for(auto& i: arr)
    {
        filters.emplace_back([&](int value){return value%i == 0;});
    }
}//此时arr已经销毁,i实际上已经成为空悬引用
int main()
{
    add_filter();
    if(filters[1](18))//此时filters内的func所使用的除数是空悬引用而非期望的 2
    {
        std::cout<< "can be diveded by 2 without remain" << std::endl;
    }
    return 0;
}
  • lambda在类内使用时,会隐式捕获this指针,当我们在类销毁后使用lambda函数对象时实际上lambda表达式此时持有一个空悬指针.在c++11标准中, lambda只能捕获this指针地址,而不能对其所指的对象进行深拷贝.这往往导致线程安全问题.
    两个陷阱的本质都是lambda的生命周期长于其函数体内所使用变量的生命周期所导致的错误.

避免此类错误的手段一般是避免使用隐式捕获以及默认捕获模式,在capture子句中写明要捕获的变量和捕获方式,以使我们或其他合作开发者在开发时对其保持敏感.

以上.

reference:

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容