C++ 的 lambda表达式

C++中一共有5种调用对象:函数函数指针重载了函数调用运算符的类(仿函数)bind创建的对象lambda表达式

函数指针

仿函数

lambda表达式

没有lambda的话,函数对象的定义太麻烦了,你得定义一个类,重载operator(),然后再创建这个类的实例。所以lambda表达式可以看成是函数对象的语法糖,在你需要的时候,它可以很简洁地给你生成一个函数对象。

语法格式
[capture list] (param list) -> return type  { function body  }

[capture list]是一个所在函数中定义的局部变量(非static)的列表,param listreturn typefunction body和普通的函数一样表示返回类型(尾置返回),参数列表,和函数体。

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

auto f = [] {  return 42;  }    // 必须包含捕获列表和函数体。

当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。默认情况下,lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员,在lambda创建时被初始化。

向lambda传递参数

与一个普通函数类似,调用一个lambda时给定的实参被用来初始化lambda的形参。通常,实参和形参类型必须匹配。但与普通函数不同,lambda不能有默认函数参数。

使用捕获列表
  • 值捕获
    采用值捕获的前提是 变量可以被拷贝,另外被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝
    void func
    {
        int v1 = 42;
        auto f = [v1] {  return v1;  }
        v1 = 0;
        auto j = f();      // j = 42,  v1在lambda创建时拷贝,而不是调用时拷贝
    }
    
  • 引用捕获
    采用引用捕获时,必须确保被引用的对象在lambda执行的时候是存在的(lambda捕获的都是局部变量,这些变量在函数结束后就不存在了)
    void func()
    {
        int val = 42;
        auto f = [&val]() {   return val; };
        val = 0;
        auto j = f();      // j = 0,引用捕获。
    }
    
  • 隐式捕获
    除了显示列出我们希望使用来自函数的变量之外,我们可以让编译器隐式推断lambda体中的代码来推断我们使用了哪些变量。为了指示编译器推断捕获列表,我们应在捕获列表写&和=,
    • &告诉编译器采用引用捕获方式
    • =表示采用值捕获的方式。
    wc = find_if(words.begin(),  words.end(),  [=](const string &s) {  return s.size() > sz;  })
    
可变lambda
  • mutable
    对于lambda表达式。默认情况下,lambda不会改变其值,如果我们希望能改变一个被捕获变量的值,就必须在参数列表首加上关键字mutable。
    void func()
    {
        int val = 42;
        // auto f = [val]() mutable { return ++val;   };    // 编译错误,v1只读。
        auto f = [val]() mutable {    return ++val;   };    // 加上mutable关键字,编译正确。
        int j = f();    // j = 43
        cout << j << " " << val << endl;    // 输出 43 和 42
    }
    

加上mutable关键字后,值捕获也会改变被捕获变量的值。

返回类型

默认情况下,如果一个lambda体包含return之外的任何语句,编译器假定此lambda返回void。所以此时我们就要显示指定尾指返回类型。

transfrom(v.begin(), v.end(), 
          [](int i) {  
              if (i < 0) 
                  return -i; 
              else 
                  return i;  
          })  // 错误
transfrom(v.begin(), v.end(), 
          [](int i) -> int {  
              if (i < 0) 
                  return -i; 
              else 
                  return i;  
          })

参数绑定bind函数

我们需要在一个std::vector中寻找大于某长度单词。那么我们可以这样写:

auto it = find_if(vec.begin(), vec.end(), 
                  [](const std::string& s) {    
                      return (s.size() > 5);    
                  });

同样,我们可以用函数去实现。

bool check_size(const std::string& s)
{
    return s.size() > 5;
}
auto it = find_if(vec.begin(), vec.end(), check_size);

但假设,我们想要指定长度来筛选。那用函数是不能实现的。

std::string::size_type sz = 5;
auto it = find_if(vec.begin(), vec.end(), 
                  [sz](const std::string& s) {  
                      return (s.size() > sz);   
                  });

bool check_size(const std::string& s, std::string::size_type sz)
{
    return s.size() > sz;
}

但是这个函数不用你管作为find_if的参数,因为find_if接受的是一元谓词。

// find_if 可能实现
template
InputIt find_if(InputIt first, InputIt last, UnaryPredicate p)
{
    for (; first != last; ++first) {
        if (p(*first)) {    // p只接受一个参数
            return first;
        }
    }
    return last;
}

但我们可以标准库的bind函数,它定义在头文件functional中,可以将它看成一个通用的函数适配器,它接受一个可调用的对象,生成一个新的可调用对象来适应原对象的参数列表

auto newCallable = bind(callable, arg_list);

那么现在可以这样写:

auto it = find_if(vec.begin(), vec.end(), bind(check_size, std::placegolders::_1, sz));

此处的bind调用生成一个可调用对象,将check_size的第二个参数绑定到sz的值,当find_if对vec中的std::string调用这个对象时。它会将给定的参数std::stringsz传递给check_size函数。

  • 使用placegolders名字
    名字_n都定义在名为placegolders的命名空间中,而这个命名空间本身定义在std命名空间中。它表示占位符,意味着将自己第n个参数按照顺序传递给原调用对象。

    auto g = bind(f, a, b, std::placegolders::_2, c, std::placegolders::_1);
    g(x, y) == f(a, b, y, c, z);
    

    在上面的例子中,g表示一个有两个参数的新的调用对象,原调用对象f有5个参数。g的第一个参数是f的第5个参数,g的第2个参数是f的第3个参数。

  • 绑定引用参数
    默认情况下,bind那些不是占位符的参数被拷贝到bind返回的可调用对象中。但是和lambda一样,有时对绑定的参数我们希望以引用的方式传递,或是要绑定的参数类型无法拷贝。

    例如,我们希望在打印每个vec中的单词后输出一个换行。

    for_each(vec.begin(), vec.end(), [&os, c](const std::string& s){  os << s << c;  });
    
    // 很容易编写一个对应的函数版本:
    ostream& print(ostream& os, const std::string& s, char c)  {  return os << s << c;  } 
    // 错误:不能拷贝os
    for_each(vec.begin(), vec.end(), bind(print, os, _1, ' '));
    

    那么,这时我们希望传递给bind的是一个对象而又不拷贝它,那么就必须使用refcref函数。它返回一个对象的引用。

    for_each(vec.begin(), vec.end(), bind(print, std::ref(os), _1, ' '));
    

你可能感兴趣的:(C++ 的 lambda表达式)