学习一下关于C++基础知识std::bind

1简介

1.1头文件

1#include <functional>

1.2函数原型

 1/**
 2   *  std::bind的函数模板
 3   */
 4template<typename _Func, typename... _BoundArgs>
 5inline _GLIBCXX20_CONSTEXPR typename
 6_Bind_helper<__is_socketlike<_Func>::value, _Func, _BoundArgs...>::type
 7    bind(_Func&& __f, _BoundArgs&&... __args)
 8{
 9    typedef _Bind_helper<false, _Func, _BoundArgs...> __helper_type;
10    return typename __helper_type::type(std::forward<_Func>(__f),
11                                        std::forward<_BoundArgs>(__args)...);
12}
13
14/**
15   *  std::bind<R>的函数模板
16   */
17template<typename _Result, typename _Func, typename... _BoundArgs>
18inline _GLIBCXX20_CONSTEXPR
19typename _Bindres_helper<_Result, _Func, _BoundArgs...>::type
20    bind(_Func&& __f, _BoundArgs&&... __args)
21{
22    typedef _Bindres_helper<_Result, _Func, _BoundArgs...> __helper_type;
23    return typename __helper_type::type(std::forward<_Func>(__f),
24                                        std::forward<_BoundArgs>(__args)...);
25}

1.3参数

std::bind返回一个基于f的函数对象,其参数被绑定到args上。

f的参数要么被绑定到值,要么被绑定到placeholders(占位符,如_1, _2, …, _29)。

什么是占位符

在C++中,占位符通常是指模板中的占位类型或占位值。在模板中,我们可以使用占位类型来表示将在实例化时替换为实际类型的类型参数。例如,我们可以定义一个模板类,其中的T就是一个占位类型:

1template <typename T>
2class MyClass
3{
4  //类的定义  
5};

在实例化时,我们可以指定T的具体类型,例如int、float等,来创建一个具体的类:

1MyClass<int> obj;//使用int作为占用符

此外,在C++中,我们还可以使用占位值来表示将在函数调用或模板实例化时替换为实际值的参数。例如,我们可以定义一个函数,其中的参数使用占位值:

1void printNumber(int number){
2    cout << "This number is " << number << endl;
3}

在调用函数时,我们可以传递具体的值作为参数:

1int number = 10;
2printNumber(number);

这些是C++中常见的占位符的一些示例。请注意,占位符的具体使用方式可能会根据上下文和具体的编程需求而有所不同。

std::bind将可调用对象与其参数一起进行绑定,绑定后的结果可以使用std::function保存。

std::bind是一个标准函数对象,作为一个函数对象适配器(function adaptor),可接受一个函数作为输入,并可绑定一个或多个被引用函数中的形参(同时允许将他们重新排列),最终返回一个新的函数对象作为输出

通俗一点,也就是可以将设计好的函数和它所需要用到的形参(传入的形参位置可以调控)绑定起来作为一个单独对象(这种处理方法多用在多线程上)

其中

关于默认的占位符有29个,即超过29个的占位符是不合法的

 1namespace placeholders
 2{
 3/* Define a large number of placeholders. There is no way to
 4 * simplify this with variadic templates, because we're introducing
 5 * unique names for each.
 6 */
 7  extern const _Placeholder<1> _1;
 8  extern const _Placeholder<2> _2;
 9  extern const _Placeholder<3> _3;
10  extern const _Placeholder<4> _4;
11  extern const _Placeholder<5> _5;
12  extern const _Placeholder<6> _6;
13  extern const _Placeholder<7> _7;
14  extern const _Placeholder<8> _8;
15  extern const _Placeholder<9> _9;
16  extern const _Placeholder<10> _10;
17  extern const _Placeholder<11> _11;
18  extern const _Placeholder<12> _12;
19  extern const _Placeholder<13> _13;
20  extern const _Placeholder<14> _14;
21  extern const _Placeholder<15> _15;
22  extern const _Placeholder<16> _16;
23  extern const _Placeholder<17> _17;
24  extern const _Placeholder<18> _18;
25  extern const _Placeholder<19> _19;
26  extern const _Placeholder<20> _20;
27  extern const _Placeholder<21> _21;
28  extern const _Placeholder<22> _22;
29  extern const _Placeholder<23> _23;
30  extern const _Placeholder<24> _24;
31  extern const _Placeholder<25> _25;
32  extern const _Placeholder<26> _26;
33  extern const _Placeholder<27> _27;
34  extern const _Placeholder<28> _28;
35  extern const _Placeholder<29> _29;
36}

另外,还有占位符的判断

1)is_placeholder判断T是否为占位符

1#include <functional>
2template<int _Num>
3struct is_placeholder<const _Placeholder<_Num> >
4    : public integral_constant<int, _Num>
5    { };

具体例子

 1#include <iostream>     // std::cout, std::boolalpha
 2#include <functional>   // std::is_placeholder, std::placeholders
 3using namespace std;
 4int main () {
 5    std::cout << std::is_placeholder<decltype(placeholders::_1)>::value << '\n';
 6    std::cout << std::is_placeholder<decltype(placeholders::_2)>::value << '\n';
 7    std::cout << std::is_placeholder<decltype(placeholders::_29)>::value << '\n';
 8    std::cout << std::is_placeholder<int>::value << '\n';
 9
10    return 0;
11}

显然占位符最大的值判断为29,非占位符的话默认为0

输出

11
22
329
40

2)is_bind_expression判断是否为bind表达式

1#include <functional>
2template<typename _Tp>
3struct is_bind_expression
4    : public false_type { };

具体例子

 1#include <iostream>     // std::cout, std::boolalpha
 2#include <functional>   // std::is_placeholder, std::placeholders
 3using namespace std;
 4double Func (double x, double y)
 5{
 6    return x / y;
 7}
 8
 9int main()
10{
11    cout << boolalpha;
12    auto NewCallable = bind(Func, placeholders::_1, 2);
13    cout << NewCallable (10) << endl;
14    auto NewCallable2 = []{
15        cout << "I am lamda expression" << endl;
16    };
17    NewCallable2();
18
19    cout << is_bind_expression<decltype(NewCallable)>::value << endl;
20    cout << is_bind_expression<decltype(NewCallable2)>::value << endl;
21}

输出

15
2I am lamda expression
3true
4false

1.4作用

std::bind主要有以下两个作用:

  1. 将可调用对象和其参数绑定成一个防函数
  2. 只绑定部分参数,减少可调用对象传入的参数

2bind使用

2.1绑定普通函数

 1#include<iostream>
 2#include <functional>
 3using namespace std;
 4double Func (double x, double y)
 5{
 6    return x / y;
 7}
 8
 9int main()
10{
11    auto NewCallable = bind(Func, placeholders::_1, 2);
12    cout << NewCallable (10) << endl;
13
14}

输出

15

bind的第一个参数是函数名,普通函数做实参时,会隐式转换成函数指针。

第一个参数被占位符占用,表示这个参数以调用时传入的参数为准,在这里调用Func时,给它传入了10,其实就想到于调用Func(10,2);

尽量不要把占位符省略写成_1,_2类似这种,需要写全placeholders::_1placeholders::_2,如果一定要使用,需要加上命名空间using namespace std::placeholders;

不然可能会出现下面的错误

1error: '_1' was not declared in this scope; did you mean 'std::placeholders::_1'?

另外改进一下,bind还可以嵌套变化

 1#include<iostream>
 2#include <functional>
 3using namespace std;
 4double Func (double x, double y)
 5{
 6    return x / y;
 7}
 8
 9double Gunc (double x)
10{
11    return x * 2 + 30.0;
12}
13
14int main()
15{
16    auto NewCallable = bind(Func, placeholders::_3, bind(Gunc, placeholders::_3));
17    cout << NewCallable (10, 20, 30) << endl;
18}

输出

10.333333

这里虽然是嵌套型的bind,是一步一步拆解之后也就那么回事。

1auto NewCallable = bind(Func, placeholders::_3, bind(Gunc, placeholders::_3));
2//拆解第一步bind(Gunc, placeholders::_3),其中placeholders::_3对应30,
3auto NewCallable = bind(Func, placeholders::_3, bind(Gunc, 30));
4//拆解第二步,是外部的bind
5auto NewCallable = bind(Func, 30, bind(Gunc, 30));
6//拆解第三步,NewCallable(10,20,30)调用了仿函数,会让第一步的值为60.0,第二步的值为0.33333

关于bind写法,main函数中的内容也可以写成下面的方式,有点类似调用仿函数表达式,结果都是一致的

1auto NewCallable = bind(Func, placeholders::_3, bind(Gunc, placeholders::_3))(10, 20, 30);
2cout << NewCallable << endl;

2.2绑定一个成员函数

bind绑定类成员函数时,第一个参数表示对象的成员函数的指针,第二个参数表示对象的地址。 必须显式地指定&A::display_del,因为编译器不会将对象的成员函数隐式转换成函数指针,所以必须在A::display_del前添加&; 使用对象成员函数的指针时,必须要知道该指针属于哪个对象,因此第二个参数为对象的地址 &a;

 1#include<iostream>
 2#include <functional>
 3using namespace std;
 4class A
 5{
 6public:
 7    int display_del(int a1, int a2)
 8    {
 9        return a1 - a2;
10    }
11};
12
13int main()
14{
15    A a;
16    //placeholders::_2对应地2个占位符,placeholders::_1对应第1个占位符
17    auto NewCallable = bind(&A::display_del, &a, placeholders::_2, placeholders::_1);
18    cout << NewCallable(20, 10) << endl;
19}

输出

1-10

2.3绑定一个引用参数

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

2.3.1引用传递到bind中的参数

 1#include<iostream>
 2#include <functional>
 3using namespace std;
 4class A
 5{
 6public:
 7    //值传递
 8    int display_del(int a1, int a2, int n1, int n2)
 9    {
10        cout << "display_del n1 = " << n1 << " n2 = " << n2 << endl;
11        n1 = a1 + a2;
12        n2 = a1 + a2;
13        return a1 - a2;
14    }
15};
16
17int main()
18{
19    A a;
20    int n = 10;
21    //默认bind也是值传递,拷贝一份参数到bind中,需要引用传递需要加上ref
22    auto NewCallable = bind(&A::display_del, &a, placeholders::_2, placeholders::_1, ref(n), n);
23    cout << "result = " << NewCallable(20, 10) << " n = " <<  n << endl;
24    n = 25;
25    cout << "result = " << NewCallable(20, 10) << " n = " <<  n << endl;
26}

输出

1result = display_del n1 = 10 n2 = 10
2-10 n = 10
3result = display_del n1 = 25 n2 = 10
4-10 n = 25

可以发现只有加引用的当外面的n值变化时,会让bind中的对应引用参数也随之变化。

2.3.2函数体参数引用传递到bind中的参数

如果说可以让引用,把main方法中的变量引用到bind中的参数,而bind中的参数是实参,调用到形参实际上是对实参的拷贝,是一种值传递。我们改成引用传递又会有不一样的收获。

 1#include<iostream>
 2#include <functional>
 3using namespace std;
 4class A
 5{
 6public:
 7    int display_del(int a1, int a2, int& n, int& m)
 8    {
 9        cout << "display_del n1 = " << n << " n2 = " << m << endl;
10        n = a1 + a2;
11        m = a1 + a2;
12        return a1 - a2;
13    }
14};
15
16int main()
17{
18    A a;
19    int n = 10;
20    int m = 10;
21    auto NewCallable = bind(&A::display_del, &a, placeholders::_2, placeholders::_1, ref(n), m);
22    cout << "result = " << NewCallable(20, 10) << " n = " <<  n << " m = " << m << endl;
23    n = 25;
24    m = 25;
25    cout << "result = " << NewCallable(20, 10) << " n = " <<  n << " m = " << m << endl;
26}

输出

1result = display_del n1 = 10 n2 = 10
2-10 n = 30 m = 10
3result = display_del n1 = 25 n2 = 30
4-10 n = 30 m = 25

这个稍微看起来复杂一点,理清楚两边的引用之后就会很简单。

3总结

总的来说,bind的思想实际上是一种延迟计算的思想,将可调用对象保存起来,然后在需要的时候再调用。而且这种绑定是非常灵活的,不论是普通函数、函数对象、还是成员函数都可以绑定,而且其参数可以支持占位符。

参考

[1] 云飞扬_Dylan. C++11中的std::bind 简单易懂, 2022.

[1] 一苇渡江694. C++11新特性应用–占位符(std::placeholders std::is_placeholder std::is_bind_expression), 2016.

[1] Jinxk8. 【C++深陷】之“decltype”, 2020.