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<std::string>中寻找大于某长度单词。那么我们可以这样写:

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<class InputIt, class UnaryPredicate>
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, ' '));
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容