美文网首页C++ 11
021 lambda 表达式

021 lambda 表达式

作者: 赵者也 | 来源:发表于2020-02-23 13:25 被阅读0次

    我们可以向一个算法传递任何类别的可调用对象(callable object)。对于一个对象或个表达式,如果可以对其使用调用运算符,则称它为可调用的。即,如果 e 是一个可调用的表达式,则我们可以编写代码 e(args),其中 args 是一个逗号分隔的一个或多个参数的列表。

    C++ 中可调用对象有函数、函数指针、重载了函数调用运算符的类,以及 lambda 表达式(lambda expression)。

    一个 lambda 表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。与任何函数类似,一个 lambda 具有一个返回类型、一个参数列表和一个函数体。但与函数不同, lambda 可能定义在函数内部。一个 lambda 表达式具有如下形式:

    [capture  list](parameter  list)->  return  type  {  function  body }
    

    其中,capture list(捕获列表)是一个 lambda 所在函数中定义的局部变量的列表(通常为空); return type、parameter list 和 function body 与任何普通函数一样,分别表示返回类型、参数列表和函数体。但是,与普通函数不同,lambda 必须使用尾置返回来指定返回类型。

    我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体:

    auto f = [] {return 42;}
    

    上面的例子中,我们定义了一个可调用对象 f,它不接受参数,返回 42。

    lambda 的调用方式与普通函数的调用方式相同,都是使用调用运算符:

    cout << f() << endl; // 打印 42
    

    在 lambda 中忽略括号和参数列表等价于指定一个空参数列表。在此例中,当调用 f 时,参数列表是空的。如果忽略返回类型,lambda 根据函数体中的代码推断出返回类型。如果函数体只是一个 return 语句,则返回类型从返回的表达式的类型推断而来。否则,返回类型为 void。

    注意:如果 lambda 的函数体包含任何单一 return 语句之外的内容,且未指定返回类型,则返回 void。
    

    向 lambda 传递参数

    与一个普通函数调用类似,调用一个 lambda 时给定的实参被用来初始化 lambda 的形参。通常,实参和形参的类型必须匹配。但与普通函数不同,lambda 不能有默认参数。因此,一个 lambda 调用的实参数目永远与形参数目相等。一旦形参初始化完毕,就可以执行函数体了。

    一个带参数的 lambda 的例子:

    [](const  string  &a,  const  string  &b) { 
      return  a.size() < b.size();
    }
    

    空捕获列表表明此 lambda 不使用它所在函数中的任何局部变量。

    如下所示,可以使用此 lambda 来调用 stable_sort:

    // 按长度排序,长度相同的单词维持字典序
    stable_sort(words.begin(),  words.end(),
    [](const  string  &a,  const  string  &b) { return  a.size() < b.size();});
    

    当 stable_sort 需要比较两个元素时,它就会调用给定的这个 lambda 表达式。

    使用捕获列表

    虽然一个 lambda 可以出现在一个函数中,使用其局部变量,但它只能使用那些明确指明的变量。一个 lambda 通过将局部变量包含在其捕获列表中来指出将会使用这些变量。捕获列表指引 lambda 在其内部包含访问局部变量所需的信息。

    在本例中,我们的 lambda 会捕获 sz,并只有单一的 string 参数。其函数体会将 string 的大小与捕获的 sz 的值进行比较:

    [sz](const  string  &a)
    { return  a.size() >=  sz; };
    

    lambda 以一对 [] 开始,我们可以在其中提供一个以逗号分隔的名字列表,这些名字都是它所在函数中定义的。

    由于此 lambda 捕获 sz,因此 lambda 的函数体可以使用 sz。lambda 不捕获 words 因此不能访问此变量。如果我们给 lambda 提供一个空捕获列表,则代码会编译错误:

    // 错误:sz 未捕获
    [](const  string  &a) { return  a.size() >=  sz; };
    
    注意:一个 lambda 只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量。
    

    lambda 捕获和返回

    当定义一个 lambda 时,编译器生成一个与 lambda 对应的新的(未命名的)类类型。我们可以这样理解,当向一个函数传递一个 lambda 时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。类似的,当使用 auto 定义一个用 lambda 初始化的变量时,定义了一个从 lambda 生成的类型的对象。

    默认情况下,从 lambda 生成的类都包含一个对应该 lambda 所捕获的变量的数据成员。类似任何普通类的数据成员,lambda 的数据成员也在 lambda 对象创建时被初始化。

    值捕获

    类似参数传递,变量的捕获方式也可以是值或引用。到目前为止,我们的 lambda 采用值捕获的方式。与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在 lambda 创建时拷贝,而不是调用时拷贝:

    void  fcn1() {
      size_t  v1 = 42;//局部变量
      // 将 v1 拷贝到名为 f 的可调用对象
      auto f = [v1] { return  v1; };
      v1 = 0;
      auto j = f2(); // j 为 42; f 保存了我们创建它时 v1 的拷贝
    }
    

    由于被捕获变量的值是在 lambda 创建时拷贝,因此随后对其修改不会影响到 lambda 内对应的值。

    引用捕获

    我们定义 lambda 时可以采用引用方式捕获变量。例如:

    void  fcn2 () {
      size_t  v1 = 42; // 局部变量
      // 对象 f2 包含 v1 的引用
      auto f2 = [&v1] { return v1; };
      v1 = 0;
      auto 3 = f2(); // j 为 0; f2 保存 v1 的引用,而非拷贝
    }
    

    v1 之前的 & 指出 v1 应该以引用方式捕获。一个以引用方式捕获的变量与其他任何类型的引用的行为类似。当我们在 lambda 函数体内使用此变量时,实际上使用的是引用所绑定的对象。在本例中,当 lambda 返回 v1 时,它返回的是 v1 指向的对象的值。

    引用捕获与返回引用有着相同的问题和限制。如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在 lambda 执行的时候是存在的。lambda 捕获的都是局部变量,这些变量在函数结束后就不复存在了。如果 lambda 可能在函数结束后执行,捕获的引用指向的局部变量已经消失。

    引用捕获有时是必要的。例如,我们可能希望 biggies 函数接受一个 ostream 的引用,用来输出数据,并接受一个字符作为分隔符:

    void biggies(vector<string> &words,
                  vector<string>::size_type sz,
                  ostream &os = cout, char c = ' ') {
        for_each(words.begin(), words.end(), [&os, c](const string &s) { os << s << c; });
    }
    

    我们不能拷贝 ostream 对象,因此捕获 os 的唯一方法就是捕获其引用(或指向 os 的指针)。

    当我们向一个函数传递一个 lambda 时,就像本例中调用 for_each 那样 lambda 会立即执行。在此情况下,以引用方式捕获 os 没有问题,因为当 for_each 执行时,biggies 中的变量是存在的。

    我们也可以从一个函数返回 lambda。函数可以直接返回一个可调用对象,或者返回一个类对象,该类含有可调用对象的数据成员。如果函数返回一个 lambda,则与函数不能返回一个局部变量的引用类似,此 lambda 也不能包含引用捕获。

    建议:尽量保持 lambda 的变量捕获简单化
    
    一个 lambda 捕获从 lambda 被创建(即,定义 lambda 的代码执行时 ) 到
    lambda 自身执行(可能有多次执行)这段时间内保存的相关信息。确保
    lambda 每次执行的时候这些信息都有预期的意义,是程序员的责任。
    
    捕获一个普通变量,如 int、string 或其他非指针类型,通常可以采用简单的值
    捕获方式。在此情况下,只需关注变量在捕获时是否有我们所需的值就可以了。
    
    如果我们捕获一个指针或迭代器,或采用引用捕获方式,就必须确保在
    lambda 执行时,绑定到迭代器,指针或引用的对象仍然存在。而且,需要保
    证对象具有预期的值。在 lambda 从创建到它执行的这段时间内,可能有代码
    改变绑定的对象的值。也就是说,在指针(或引用)被捕获的时刻,绑定的对
    象的值是我们所期望的,但在 lambda 执行时,该对象的值可能已经完全不同了。
    
    一般来说,我们应读尽量减少捕获的数据量,来避免潜在的捕获导致的问题。
    而且,如果可能的话,应试避免捕获指针或引用。
    

    隐式捕获

    除了显式列出我们希望使用的来自所在函数的变量之外,还可以让编译器根据 lambda 体中的代码来推断我们要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中写一个 & 或 =。& 告诉编译器采用捕获引用方式,= 则表示采用值捕获方式。例如,我们可以重写传递给 find_if 的 lambda:

    // sz 为隐式捕获,值捕获方式
    wc = find_if(words.begin(), words.end(),
                [=](const string &s){ return s.size() >= sz;});
    

    如果我们希望对一部分变量釆用值捕获,对其他变量采用引用捕获,可以混合使用隐式捕获和显式捕获:

    void biggies(vector<string> &words, vector<string>::size_type sz, 
                  ostream &os = cout, char c = ' ') {
      // 其他处理
      // os 隐式捕获,引用捕获方式;c 显式捕获,值捕获方式
      for_each(words.begin(), words.end(), [&, c](const string &s) { os << s << c;});
      // os 显式捕获,引用捕获方式;c 隐式捕获,值捕获方式
      for_each(words.begin(), words.end(), [=, &os](const string &s) { os << s << c;});
    }
    

    当我们混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个 & 或 =。此符号指定了默认捕获方式为引用或值。

    当混合使用隐式捕获和显式捕获时,显式捕获的变量必须使用与隐式捕获不同的方式。即,如果隐式捕获是引用方式(使用 &),则显式捕获命名变量必须采取值方式,因此不能在其名字前使用 &。类似地,如果隐式捕获采用的是值方式(使用 = ),则显式捕获命名变量必须采用引用方式,即,在名字前使用 &。

    lambda 捕获列表 说明
    [] 空捕获列表。lambda 不能使用所在函数中的变量。一个 lambda 只有捕获变量后才能使用它们
    [names] names是一个逗号分隔的名字列表,这些名字都是 lambda 所在函数的局部变量。默认情况下,捕获列表中的变量都被拷贝。名字前如果使用了 &,则采用引用捕获方式
    [&] 隐式捕获列表,采用引用捕获方式。lambda 体中所使用的来自所在函数的实体都采用引用方式使用
    [=] 隐式捕获列表,采用值捕获方式。lambda 体将拷贝所使用的来自所在函数的实体的值
    [&,identifier_list] identifier_list 是一个逗号分隔的列表,包含 0 个或多个来自所在函数的变量。这些变量都采用值捕获方式,而任何隐式捕获的变量都采用引用方式捕获。identifier_list 中的名字前面不能使用 &
    [=,identifier_list] identifier_list 中的变量都采用引用方式捕获,而任何隐式捕获的变量都采用值方式捕获。identifier_list 中的名字不能包括 this,且这些名字之前必须使用 &

    可变 lambda

    默认情况下,对于一个值被拷贝的变量,lambda 不会改变其值。如果我们希望能改变一个被捕获的变量的值,就必须在参数列表首加上关键字 mutable。因此,可变 lambda 能省略参数列表:

    void fcn3() {
      size_t v1 = 42; // 局部变量
      // f 可以改变它所捕获的变量的值
      auto f = [v1]() mutable { return ++v1; };
      v1 = 0;
      auto j = f();  // j 为 43
    }
    

    一个引用捕获的变量是否(如往常一样)可以修改依赖于此引用指向的是一个 const 类型还是一个非 const 类型:

    void fcn4() {
      size_t v1 = 42; // 局部变量
      // v1 是一个非 const 变量的引用
      // 可以通过 f2 中的引用来改变它
      auto f2 = [&v1]() mutable { return ++v1; };
      v1 = 0;
      auto j = f2();  // j 为 1
    }
    

    指定 lambda 返回类型

    到目前为止,我们所编写的 lambda 都只包含单一的 return 语句。因此,我们还未遇到必须指定返回类型的情况。默认情况下,如果一个 lambda 体包含 return 之外的任何语句,则编译器假定此 lambda 返回 void。与其他返回 void的函数类似,被推断返回 void 的 lambda 不能返回值。

    下面给出了一个简单的例子,我们可以使用标准库 transform 算法和一个 lambda 来将一个序列中的每个负数替换为其绝对值:

    transform(vi.begin(), vi.end(), vi.begin(),
                [](int i) { return i < 0 ? -i : i;  });
    

    函数 transform 接受三个迭代器和一个可调用对象。前两个迭代器表示输入序列,第三个迭代器表示目的位置。算法对输入序列中每个元素调用可调用对象,并将结果写到目的位置。如本例所示,目的位置迭代器与表示输入序列开始位置的迭代器可以是相同的。当输入迭代器和目的迭代器相同时,transform 将输入序列中每个元素替换为可调用对象操作该元素得到的结果。

    在本例中,我们传递给 transform 一个 lambda,它返回其参数的绝对值。 lambda 体是单一的 return 语句,返回一个条件表达式的结果。我们无须指定返回类型,因为可以根据条件运算符的类型推断出来。

    但是,如果我们将程序改写为看起来是等价的 if 语句,就会产生编译错误:

    // 错误:不能推断 lambda的返回类型
    transform(vi.begin(), vi.end(), vi.begin(),
                [](int i) { if(i < 0) return -i; else return i;  });
    

    编译器推断这个版本的 lambda 返回类型为 void,但它返回了一个 int 值。

    当我们需要为一个 lambda 定义返回类型时,必须使用尾置返回类型:

    transform(vi.begin(), vi.end(), vi.begin(),
                [](int i) -> int { if(i < 0) return -i; else return i;  });
    

    在此例中,传递给 transform 的第四个参数是一个 lambda,它的捕获列表是空的,接受单一 int 参数,返回一个 int 值。它的函数体是一个返回其参数的绝对值的 if 语句。

    相关文章

      网友评论

        本文标题:021 lambda 表达式

        本文链接:https://www.haomeiwen.com/subject/pakznqtx.html