包装器std::function&std::bind

最近在看代码时经常看到这两个包装器的使用,一直以来对这部分内容都没有过深入的了解,借着这个机会深入系统的学习一下这两个包装器功能和使用。

std::function

思考一下为什么需要std::function?目前c++提供的可调用的对象有以下几种

  • 函数名

  • 函数对象

  • labmda表达式

  • 类成员函数

std::function的本质是一个类模板

先看下面的代码:

#include <iostream>

using namespace std;
//声明函数模板
template <typename F,typename T>
T WrapperTest(F f, T t1, T t2)
{
    static int count = 0;
    cout <<"count value = "<<count++<<endl;
    cout <<"count addr = "<<&count<<endl;
    return f(t1,t2);
}
//函数名方式
int add(int a, int b)
{
    return a+b;
}

//仿函数
struct add1{

//public:
    int operator ()(int a, int b){

        return a+b;
    }
};

int main(int argc, char *argv[])
{
    //函数名
    cout <<"function name  "<<endl;
    cout << WrapperTest(add, 5,6)<<endl;

    //仿函数(函数对象)
    cout <<"fang hanshu  "<<endl;
    cout <<WrapperTest(add1(), 7,8)<<endl;

    //labmda表示式
    cout << "lambda  "<<endl;
    cout <<WrapperTest([](int a,int b){return a+b;}, 10,19)<<endl;

    return 0;
}

代码运行结果如下:

通过代码运行结果可以看出,函数模板针对三种不同方式相同功能的代码生成了三个实例,对于模板而言降低了模板的效率。

由于函数指针、仿函数、lambda表达式是不同的类型,因此WrapperTest函数会被实例化出三份,三次调用WrapperTest函数所打印count的地址也是不同的。
但实际这里根本没有必要实例化出三份WrapperTest函数,因为三次调用WrapperTest函数时传入的可调用对象虽然是不同类型的,但这三个可调用对象的返回值和形参类型都是相同的。
使用包装器std::function可以解决该问题

那如果采用包装器看看什么效果呢?请看如下代码

#include <iostream>

using namespace std;
//声明函数模板
template <typename F,typename T>
T WrapperTest(F f, T t1, T t2)
{
    static int count = 0;
    cout <<"count value = "<<count++<<endl;
    cout <<"count addr = "<<&count<<endl;
    return f(t1,t2);
}
//函数名方式
int add(int a, int b)
{
    return a+b;
}

//仿函数
struct add1{

//public:
    int operator ()(int a, int b){

        return a+b;
    }
};

int main(int argc, char *argv[])
{
    //包装器
    function<int(int,int)> f = add;
    function<int(int,int)> f1 = add1();
    function<int(int,int)> f2 = [](int a,int b){return a+b;};
    cout<<WrapperTest(f, 4 , 4)<<endl;
    cout<<WrapperTest(f1, 4 , 5)<<endl;
    cout<<WrapperTest(f2, 4 , 6)<<endl;

    return 0;
}

运行结果如下:

对比使用包装器std::function和不使用包装器的输出,可以看到在使用包装器的情况下WrapperTest只生成了一份对象实例,而不使用包装器则使用了三个对象实例。通过static静态局部变量来表现的。

分析:

首先要明确std::function的的使用方法类似变量,std::function是一个类模板,需要在使用的时候指明函数的返回值和参数类型及个数,例如std::function<int(int, int)> f = add;f在这里是一个局部变量,需要给其赋值这个对象才有意义,std::function返回的是 一个可调用对象,与add/add1()/lambda表达式类似,是可以直接被其他程序直接调用生效的。一个std::function对象仅能绑定一个函数,不能产生一对多的情况,即不可能f的调用对象即是add又是add1()或者lambda表达式。

f(2,2); //输出结果4

思考:上面的例子使用的要么是函数名,要么是是函数对象,要么是lambda表达式,不过他们有一个共同点那就是他们不是成员函数,那么std::function如果遇到类的静态成员函数或者普通成员函数有什么区别呢?该如何使用?请看下面的代码:

class A{

public:
    A(){
        cout<<"A construct"<<endl;
    }
    ~A(){
        cout<<"A deconstruct"<<endl;
    }
    static void fun1(int a){
        cout<<"fun1 run "<<a<<endl;
    }
    void fun2(int b){
        cout<<"fun2 run "<<b<<endl;
    }
};

int main(int argc, char *argv[])
{
    //静态成员函数
    cout<<"fun1 addr = "<< A::fun1 << "   "<<&A::fun1<<endl;
    function<void(int)> f = A::fun1;
    f(5);

    //普通成员函数
    function<void(A, int)> f1 = &A::fun2;  //取地址符&可加可不加
    f1(A(), 7);

    return 0;
}

输出结果如下所示:

由此可得出结论:

  • 对于类的静态成员函数,由于该函数时所有类共有,编译器不会在参数列表中加入对象指针this,所以函数的原型就是声明的类型,故在function在使用时采用的是function<void(int)>来定义类型。

  • 对于类的普通成员函数,编译器会在编译阶段为每个普通成员函数的参数中加入一个this指针,并在该参数为参数的第一个位置,所以才看到上面的代码中声明function类型时采用的是function<void(A,int)>,从这里就可以理解为什么要加上A的类型名。调用方法为f1(A(),7);

  • 对于类的静态成员函数或者普通成员函数取地址方法都是A::fun1,在window平台下使用mingw编译器亲测加不加&都没有影响。

为什么需要function包装器呢?

答:因为function包装器有一个很重要的功能就是能够把不同的类型的调用对象统一为一种类型的调用对象,如上面例子中提到的函数名、函数对象或者lambda表达式实现的功能一样,参数和返回值也一样,但是如果他们属于不同的类型,使用function包装器后,可以让这三种类型统一,他们都是std::function<int(int,int)>类型,在后续代码实现时能够发挥很大的作用,达到优化代码的效果。详情参见如下链接:https://blog.csdn.net/m0_52169086/article/details/126959210

继续思考:bind包装器与function包装器如何配合使用呢?

这里先说一下自己的理解,std::bind包装器返回的同样的是一个可调用对象,该对象既可以操作普通函数,函数对象,lambda表达式,又可以操作类成员函数。

std::bind的本质也是一个函数模板,函数模板的使用与函数类似,需要传入具体的参数。他的返回值一般是function包装的好的类型,例如:

int add(int a,int b)
{
    return a+b;
}
std::function<int (int,int)> f = bind(add,5,placeholders::_1);//固定第一个参数为5,第二个参数
为f调用时传入的第一个参数。

上面的代码说明了一些东西:

  • placeholders::_x是站位符,代表的是新生成的可调用对象f发生调用时输入的第几个参数,_1代表的是第一个参数,_2代表的是第二个参数,以此类推。这个占位符只与新生成的可调用对象f有关,与原函数add无关。

  • 那么问题来了,在bind函数的第二个参数开始的参数顺序代表了什么呢?bind参数的第二个参数是第一个可调用对象add的第一个参数,在这里固定为5了;bind参数的第三个参数为add的第二个参数,以此类推。

上面讨论的是c风格的bind函数使用,因为在c++中进行开发,总是不可避免的需要和类打交道,我们必须要非常清晰的知晓bind包装器在做与类相关的绑定时应该如何做?应该注意哪些事项。

类的可调用对象只有两种:静态成员函数与普通成员函数。所以我们只需要分析这两种情况即可。

  • 静态成员函数,

class A{

public:
    A(){
        cout<<"A construct"<<endl;
    }
    ~A(){
        cout<<"A deconstruct"<<endl;
    }
    static void fun1(int a){
        cout<<"fun1 run "<<a<<endl;
    }
    void fun2(int b){
        cout<<"fun2 run "<<b<<endl;
    }
};

int main(int argc, char *argv[])
{
    //静态成员函数
    function<void(int)> f = bind(A::fun1,placeholders::_1);
    f(8);
    return 0;
}

运行结果如下:

结论:对于bind包装器对于静态成员函数的使用,由于返回的对象一般是function对象,并且fun1的函数原型就是不带this指针的,所以bind时仅需要传递函数的地址(如何获取静态成员函数的地址前面已经做了说明),然后使用占位符配置参数即可。需要注意的是定义functtion对象时一定要参考函数的实际类型,如果静态成员函数不包含this指针,所以定义function对象时肯定不能定义成function<void(A,int)>。

  • 普通成员函数

class A{

public:
    A(){
        cout<<"A construct"<<endl;
    }
    ~A(){
        cout<<"A deconstruct"<<endl;
    }
    static void fun1(int a){
        cout<<"fun1 run "<<a<<endl;
    }
    void fun2(int b){
        cout<<"fun2 run "<<b<<endl;
    }
};

int main(int argc, char *argv[])
{
    A a;
    function<void(int)> f = bind(&A::fun2,&a, placeholders::_1);
    f(9);
    return 0;
}

通过上面的结果可以看出类的普通成员函数bind的方法和对应的function对象的类型。

结论:对于普通成员函数使用bind包装器包装为一个新的可调用对象时,bind的第一个参数的是类的普通成员函数的地址,第二个参数必须传入一个该类对象的地址(这里传入的是a对象的地址),从第三个参数开始才为函数的参数。这些都是理解的

通过这个说明了什么呢?说明可以通过bind包装器通过类的普通成员函数生成的可调用对象的类型与函数名、函数对象或者lambda表达式生成的可调用对象的类型统一

但是有一个问题:为什么对普通成员函数进行bind后生成的可调用对象是function<void(int)>,为什么不是想象中的function<void(A,int)>呢?为什么前面在讲述funciton包装器的时候普通成员函数的类型必须是function<void (A, int)>?

答:这个问题可以这样理解,bind包装器对普通成员函数的包装时已经将this指针作为第一个参数传入到了函数中(其实就是this,因为普通成员函数的调用一定要指定对象,否则无法进行调用),相当于固定参数,由于是固定参数(举个例子,functin<int<int>>f = bind(add,1,placeholders::_1),add有两个参数,但是生成的调用对象f就只有一个参数了),所以声明function对象类型的时候时候就不需要写明他的类型了。

而不利用bind包装器的情况下只是将普通成员函数与function类型绑定,由于无法传递this指针到函数中,所以必须显示的在类型中声明this指针对应的参数。即function<void(A,int)>。

bind包装器的作用

前面讲述了很多,那bind到底有什么用呢?通过参考其他一些大神的介绍,一般情况下使用bind的场景是减少参数的时候可以考虑使用,例如add函数包含两个参数,现在需要将其中第一个参数固定,这个时候可以考虑使用bind包装器生成新的调用对象,使用新的调用对象传入一个参数即可,可以参考前面的例子。

在类中使用function时一般与bind组合使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值