彻底搞懂bind与lambda表达式

本篇博客来自于深蓝学院c++课程,自己整理总结得到。

可调用对象

  • 函数指针:概念直观,但定义位置受限,因为c++不支持函数中定义函数
  • :功能强大,但是写起来麻烦
  • bind:基于已有的逻辑灵活适配,但在描述复杂的逻辑时语法比较复杂难懂
  • lambda表达式:小巧灵活,功能强大

bind方法使用

bind:通过绑定的方式修改可调用对象的调用方式

  • std::bind ( C++11 引入):用于修改可调用对象的调用方式
    – 调用 std::bind 时,传入的参数会被复制,这可能会产生一些调用风险
    – 可以使用 std::ref 或 std::cref 避免复制的行为

案例1:bind基本使用

#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>

bool MyPredict(int val1, int val2)
{
    return val1 > val2;
}
int main(int argc, char**argv)
{
    using namespace std::placeholders;
    std::vector<int> x{1,2,3,4,5,6,7,8,9};
    std::vector<int> y;
    std::copy_if(x.begin(), x.end(), std::back_inserter(y), std::bind(MyPredict, _1 , 3));
    for (auto value : y)
    {
        std::cout <<value << " ";
    }
    std::cout << std::endl;
    // 结果为:4 5 6 7 8 9 
}

深度解析bind参数含义

这里,单独来探究bind中传入参数的意义。仍然以案例1来进行分析,首先看下面这段程序:

#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>

bool MyPredict(int val1, int val2)
{
    return val1 > val2;
}
int main(int argc, char**argv)
{
    using namespace std::placeholders;
    auto x = std::bind(MyPredict, _1, 3);
    std::cout << x(50) << std::endl; // 1

    auto x = std::bind(MyPredict, 3, _1);
    std::cout << x(50) << std::endl; // 0
}

bind中的_1(std::placeholders::_1)是绑定的传入x(50)的第一个参数并不是MyPredict中的val1。std::bind(MyPredict, 3, _1)中3对应的是MyPredict的va1,_1对应MyPredict的val2,也就是说bind中参数的顺序对应绑定的可调用对象的参数顺序,但是std::placeholders::_1绑定的是传入x的第一个参数,这点非常重要。

为了进一步理解,看下面这段代码:

bool MyPredict(int val1, int val2)
{
    return val1 > val2;
}
int main(int argc, char**argv)
{
    using namespace std::placeholders;
    auto x = std::bind(MyPredict, _2, 3);
    std::cout << x(1, 50) << std::endl; // 1
}

这里的返回结果仍然为1,原因是,_2绑定的传入x的第二个参数50。
进一步,我们可以看到:

bool MyPredict(int val1, int val2)
{
    return val1 > val2;
}
int main(int argc, char**argv)
{
    using namespace std::placeholders;
    auto x = std::bind(MyPredict, _2, _1);
    std::cout << x(3, 4) << std::endl; // 1
}

这里返回1的原因,和上面原理一模一样,_2即传入x(3,4)的第二个参数4.与此同时_2绑定的是MyPredict的val1。同理分析val2。

案例2:bind进阶用法

bool MyPredict(int val1, int val2)
{
    return val1 > val2;
}

bool MyAnd(bool val1, bool val2)
{
    return val1 && val2;
}

int main(int argc, char**argv)
{
    using namespace std::placeholders;
    auto x1 = std::bind(MyPredict, _1, 3);
    auto x2 = std::bind(MyPredict, 10, _1);
    auto x3 = std::bind(MyAnd, x1, x2);
    std::cout << x3(5); // 1
}

结果分析:先会处理x1,在处理x2,x1和x2返回的值在带入到MyAnd。实则这个程序在判断给的结果是否在3和10之间。

bind的注意事项

1.调用 std::bind 时,传入的参数会被复制,这可能会产生一些调用风险

bool MyPredict(int val1, int val2)
{
    return val1 > val2;
}

bool MyAnd(bool val1, bool val2)
{
    return val1 && val2;
}

void MyProc(int* ptr) {}

auto fun()
{
    int x;
    return std::bind(MyProc, &x);
}


int main(int argc, char**argv)
{
    auto ptr = fun();
    ptr(); // 行为未定义
}

结果分析:这个代码存在风险,ptr()的行为未定义。具体原因为:std::bind(MyProc, &x)返回一个可调用对象,但是&x指向的是一个局部变量并且x地址会被复制到bind对象中,当fun()函数结束时,局部变量已经为销毁了。
解决方法:智能指针

void MyProc(std::shared_ptr<int> ptr) {}

auto fun()
{
    std::shared_ptr<int> x(new int());
    return std::bind(MyProc, x);
}


int main(int argc, char**argv)
{
    auto ptr = fun();
}

2.使用 std::ref 或 std::cref 避免复制的行为

先看一段代码:

void Proc(int& x)
{
    ++x;
}

int main(int argc, char**argv)
{
    int x = 0;
    auto b = std::bind(Proc, x);
    b();
    std::cout << x << std::endl; // 0
}

结果分析:我们想得到的应该是1,但是结果为0。原因就是x是被拷贝复制到b中,可以理解为b中的x被修改,但是原本的x并没有发生变化。
解决方法: std::ref 或 std::cref

void Proc(int& x)
{
    ++x;
}

int main(int argc, char**argv)
{
    int x = 0;
    auto b = std::bind(Proc, std::ref(x));
    b();
    std::cout << x << std::endl; // 1
}

lambda表达式

  • lambda表达式的基本组成部分
    –参数与函数体
    –返回类型
    –捕获:针对函数体中使用的局部自动对象进行捕获

    • 值捕获、引用捕获与混合捕获
    • this捕获
    • 初始化捕获(c++14)
    • *this捕获(c++17)

    –说明符

    • mutable/constexpr(c++17)
  • lambda表达式的深度应用
    – 捕获时计算(c++ 14)
    – 即调用函数表达式
    – 使用auto避免赋值(c++ 14)
    – Lifting(c++ 14)
    – 递归调用(c++14)

案例1:lambda表达式的基本使用:参数与函数体

#include <iostream>

int main(int argc, char**argv)
{
    auto x = [](int val){return (val > 3) && (val < 10);};
    std::cout << x(5) << std::endl; // 1
}

案例2:lambda表达式的基本使用:返回类型

c++11开始可以自动推导lambda表达式的返回类型,但是要保证lambda表达式内部的返回值一致,这样才可以自动推导,如果不一致,就必须显示指定返回类型。来看一个例子:

int main(int argc, char**argv)
{
    auto x = [](int val) -> float 
    {
        if (val > 5) return 3.0;
        else return 1.5f;
    };
}

如果这里不显示指定返回类型为float,程序会报错。因为3.0是double类型,1.5f是float类型,编译器无法完成自动类型推导。

案例3:lambda表达式的捕获(非常重要)

1. 局部自动对象进行捕获

先来看一个报错的例子,如下:

int main(int argc, char**argv)
{
    int y = 10;
    auto x = [](int val) {return val > y;};
}

错误为:

error: ‘y’ is not captured

分析一下原因:因为lambda表达式里面用到了y,但是y是一个局部变量并且在lambda表达式函数体外部,lambda表达式是无法访问的,所以报错。那怎么解决呢?其实很简单,捕获y即可,具体操作如下:

int main(int argc, char**argv)
{
    int y = 10;
    auto x = [y](int val) {return val > y;};
    std::cout << x(3) << std::endl; // 0
}

下面,继续讨论一下,我们要知道捕获的是局部自动对象,那如果y是全局变量或者静态变量,可不可以不捕获,直接在lambda表达式内部进行使用呢?答案是可以。因此,针对报错代码,还可以这么进行修改。

int main(int argc, char**argv)
{
    static  int y = 10;
    auto x = [](int val) {return val > y;};
    std::cout << x(3) << std::endl; // 0
}
int y = 10;
int main(int argc, char**argv)
{

    auto x = [](int val) {return val > y;};
    std::cout << x(3) << std::endl; // 0
}

所以,一定要明白,lambda表达式捕获的是局部自动对象。

2. 值捕获

int main(int argc, char **argv) {
    int y = 10;
    auto x = [y](int val) mutable
    {
        ++y;
        return val > y;
    };
    std::cout << x(3) << std::endl; // 0
    std::cout << y << std::endl; // 10
}

分析一下:先不要管说明符mutable,后续讨论。这个程序的运行逻辑是:线对y执行加1操作,然后再将val于y进行比较。但是我们可以看到y的返回值仍然是10,不是11。这是为啥呢?原因是:值捕获是将捕获的对象复制到lambda表示式内部(或者说类内部,因为lambda表达式的本质是类,编译器是将lambda表达式翻译成类的形式,这也是lambda表达式功能强大的重要原因),那我们知道lambda表达式内部执行的++y,实则只是对复制到类内部的y进行加1操作,对外部定义的y没有进行任何操作,因此返回值仍然是10。如果大家不相信,觉得这个很神奇,可以借用**c++ insights**进行查看,这里我也展示一下吧。

int main(int argc, char ** argv)
{
  int y = 10;
    
  class __lambda_4_14
  {
    public: 
    inline /*constexpr */ bool operator()(int val)
    {
      ++y;
      return val > y;
    }
    
    private: 
    int y;
    
    public:
    __lambda_4_14(int & _y)
    : y{_y}
    {}
    
  };
  
  __lambda_4_14 x = __lambda_4_14{y};
  return 0;
}

这段代码其实就是c++内部对lambda进行的处理,很显然可以看出来在类的内部定义了一个成员变量为y,然后捕获的y是复制给成员变量y,那么y本身肯定是没有发生任何改变的。

3. 引用捕获

与值捕获恰恰相反的是引用捕获,我们来看看上面那个例子:

int main(int argc, char **argv) {
    int y = 10;
    auto x = [&y](int val)
    {
        ++y;
        return val > y;
    };
    std::cout << x(3) << std::endl; // 0
    std::cout << y << std::endl; // 11
}

分析一下,这里返回的值就是11了,不在是10了。这是为啥呢,在此借用c++ insights看一下就一目了然了哈。

int main(int argc, char ** argv)
{
  int y = 10;
    
  class __lambda_4_14
  {
    public: 
    inline /*constexpr */ bool operator()(int val) const
    {
      ++y;
      return val > y;
    }
    
    private: 
    int & y;
    
    public:
    __lambda_4_14(int & _y)
    : y{_y}
    {}
    
  };
  
  __lambda_4_14 x = __lambda_4_14{y};
  return 0;
}

大家看出来区别了吗?与值捕获不同的是,类内部是int&,那么在把y传给类内部的成员变量时,相当于是外部y的一个别名,那么在进行++操作时,对外部的y也进行了++操作,因此返回11。这下就一目了然了。

4. 混合捕获

先看一个简单的例子,知道啥叫混合捕获

int main(int argc, char **argv) {
    int y = 10;
    int z = 3;
    auto x = [&y, z](int val) mutable 
    {
        ++y;
        ++z;
        return val > z;
    };
    std::cout << x(3) << std::endl; // 0
    std::cout << y << std::endl; // 11
    std::cout << z << std::endl; // 3
}

注意观察捕获列表,对y是引用捕获,返回值为11,对z是值捕获,返回值为3。这就是混合捕获了。
有的时候,我们可能需要捕获很多局部自动对象,这时候在一个个写太麻烦了,那么这是怎么办呢?

int main(int argc, char **argv) {
    int y = 10;
    int w = 10;
    int z = 3;
    auto x = [&, z](int val) mutable
    {
        ++y;
        ++z;
        ++w;
        return val > z;
    };
    std::cout << x(3) << std::endl; // 0
    std::cout << y << std::endl; // 11
    std::cout << z << std::endl; // 3
    std::cout << w << std::endl; // 11
}

在观察捕获列表[&, z],代表的含义为,除了z为值捕获,其他均为引用捕获。那么,大家可以联想很多了,假如是[&]就代表所有对象都是引用捕获,[=, &z]代表除了z是引用捕获,其他都是值捕获。以后遇到了其他的,以此类推即可。

5. this捕获

struct Str
{
    auto fun()
    {
        int val = 3;
        auto lam = [val, this]()
        {
            return val > x;
        };
        return lam();
    }
    int x;
};

int main(int argc, char **argv)
{
    Str s;
    s.fun();
}

分析一下,因为结构体中定义的x不是局部自动对象(因为没有定义在函数fun()内部),因此之前介绍的捕获方法都无法使用,这时候就需要使用this捕获,this其实是一个指针,指向Str这个地址,当我们调用fun()函数时,this指向的Str地址中有变量x,此时就可以捕获了。

6. 初始化捕获(c++14)

优点:(1)可以引入复杂的捕获逻辑;(2)一定程度提升系统性能

简单使用:

int main(int argc, char **argv)
{
    int x = 3;
    auto lam = [y = x](int val)
    {
        return val > y;
    };
    std::cout << lam(100) << std::endl; // 1
}

捕获列表中的=不是值捕获,具体含义为:先构造一个局部自动对象y,然后将x赋给y,然后val于y在进行比较

优点1:复杂捕获逻辑

int main(int argc, char **argv)
{
    std::string a = "hell0";
    auto lam = [y = std::move(a)]()
    {
        std::cout << y << std::endl;
    };
    std::cout << a << std::endl; // 空
    lam(); // hello
}

注意一下,这里的a为空是发生在lam()调用之前。

优点2:一定程度提升系统性能

int main(int argc, char **argv) {
    int x = 10;
    int y = 20;
    auto lam = [z = x + y](int val) {
        return val > z;
    };
    lam(3); 
}

这段代码的目的是计算x+y的值然后与传入的val进行比较,如果使用初始化捕获,x+y的值会被保存在z中,每次调用只会将z与val进行比较。但是,如果使用之前的方式,代码如下:

int main(int argc, char **argv) {
    int x = 10;
    int y = 20;
    auto lam = [x, y](int val) {
        return val > (x + y);
    };
    lam(3);
}

此时,程序执行的逻辑是,每次调用都需要计算x+y然后再进行比较。很显然,初始化捕获可以提升系统的性能。

7. *this捕获

struct Str {
    auto fun() {
        int val = 3;
        auto lam = [val, *this]() {
            return val > x;
        };
        return lam;
    }
    int x;
};

auto wrapper()
{
    Str s;
    return s.fun();
}

int main(int argc, char **argv) {
    auto lam = wrapper();
    lam();
}

这段代码与捕获this非常相似,但是本质完全不同。*this是解引用,是Str对象,this是指向Str的对象,*this是通过复制Str对象到lambda表达式内部,但是这样也会带来问题,复制会占用资源。如果这里用this,那么在调用wrapper()时,lam是一个悬挂的指针,指向了已经销毁的对象,这是因为如果使用this,那么this指向的是Str s,这是一个局部自动对象,在调用wrapper()之后就已经被销毁了。

说明符

1. mutable

可以对类内部的成员进行修改

int main(int argc, char **argv) {
    int y = 10;
    auto lam = [y](int val) mutable 
    {
        ++y;
        return val > y;
    };
}

我们在c++ insights中看一下:

int main(int argc, char ** argv)
{
  int y = 10;
    
  class __lambda_3_16
  {
    public: 
    inline /*constexpr */ bool operator()(int val)
    {
      ++y;
      return val > y;
    }
    
    private: 
    int y;
    
    public:
    __lambda_3_16(int & _y)
    : y{_y}
    {}
    
  };
  
  __lambda_3_16 lam = __lambda_3_16{y};
  return 0;
}

重载()运算符后面没有const,但是如果没有mutable说明符,在观察一下:

int main(int argc, char ** argv)
{
  int y = 10;
    
  class __lambda_3_16
  {
    public: 
    inline /*constexpr */ bool operator()(int val) const
    {
      return val > y;
    }
    
    private: 
    int y;
    
    public:
    __lambda_3_16(int & _y)
    : y{_y}
    {}
    
  };
  
  __lambda_3_16 lam = __lambda_3_16{y};
  return 0;
}

可以明显看到,bool operator()(int val) const 有一个const,说明无法对类内部的数据进行改变,因为也就无法实现++y的功能。

2. constexpr c++17

好处:可以在编译期进行调用

int main(int argc, char **argv) {
    auto lam = [](int val) constexpr 
    {
        return val + 1;
    };
    constexpr int val = lam(100);
    std::cout << val << std::endl;
}

lambda表达式的深入应用

1. 捕获时计算 c++14

int main(int argc, char **argv) {
    int x = 3, y = 5;
    auto lam = [z = x + y]()
    {
        return z;
    };
    std::cout << lam() << '\n'; // 8
}

2. 即调用函数表达式(IIFE)

优势:可以初始化一些常量

int main(int argc, char **argv) {
    int x = 3, y = 5;
    const auto val = [z = x + y]()
    {
        return z;
    }();
}

3. 使用auto避免复制 c++14

#include <iostream>
#include <map>

int main(int argc, char **argv) {
    std::map<int, int> m{{2,3}};
    auto lam = [](const auto& p)
    {
        return p.first + p.second;
    };
    std::cout << lam(*m.begin()) << '\n'; // 5
}

如果这里不得auto,写成下面代码:

int main(int argc, char **argv) {
    std::map<int, int> m{{2, 3}};
    auto lam = [](const std::pair<int, int> &p) {
        return p.first + p.second;
    };
    std::cout << lam(*m.begin()) << '\n'; // 5
}

此时在调用的时候会产生多余的复制操作,程序将*m.begin()复制给p。

4. Lifting c++14

auto fun(int val)
{
    return val + 1;
}

// 函数重载
auto fun(double val)
{
    return val + 1;
}

int main(int argc, char **argv) {
    auto lam = [](auto x)
    {
        return fun(x);
    };
    std::cout <<lam(3) << '\n'; // 4
    std::cout <<lam(3.5) << '\n'; // 4.5
}

5. 递归调用 c++14

int main(int argc, char **argv) {

    auto factorial = [](int n) {
        auto f_impl = [](int n, const auto &impl) -> int {
            return n > 1 ? n * impl(n - 1, impl) : 1;
        };
        return f_impl(n, f_impl);
    };
    std::cout << factorial(5) << std::endl; // 120
}
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值