C++11绑定器bind及function机制

时间:2022-10-31 07:13:55

前言

之前在学muduo网络库时,看到陈硕以基于对象编程的方式,大量使用boost库中的bindfunction机制,如今,这些概念都已引入至C++11,包含在头文件<functional>中。

本篇文章主要梳理C++绑定器相关的内容以及C++11中引入的function机制,其中绑定器主要有三种:bind1stbind2ndbind(C++11)。学完本篇内容,将对C++绑定器及function机制等的底层实现有深刻理解,那么我们开始说吧。

函数对象

首先说说函数对象,之所以说函数对象,是因为绑定器、function都涉及到该部分概念。函数对象实际上是类调用operator()()小括号运算符重载,实现像在“调用函数”一样的效果,因此还有个别名叫“仿函数”。函数对象示例代码如下:

class Print {
public:
    void operator()(string &s) { cout << s << endl; }
};

int main() {
    string s = "hello world!";
    Print print; //定义了一个函数对象print
    print(s);
    return 0;
}

上面代码print(s);语句,看似像函数调用,其实是类对象print调用其小括号运算符重载print.operator(string &s)print就是一个函数对象,至此对函数对象就有了基本的认识。

剖析绑定器bind1st、bind2nd

了解了函数对象,接下来我们说说绑定器,为什么需要绑定器?在使用STL时经常会遇到STL算法中需要传递某元函数对象,比如在写sort时,第三个参数决定了我们的排序规则,用来接收一个“比较器”函数对象,该函数对象是一个二元的匿名函数对象,形如greator<int>()或者less<int>()。二元函数对象的意思是,这个函数对象的小括号运算符重载函数接收两个参数,那么几元就表示接收几个参数。下面是库中自带的greaterless模板类的源码实现,可以看到是对小括号运算符重载的实现,sort第三个参数接收该模板类的二元匿名函数对象。

  template<typename _Tp>
    struct greater : public binary_function<_Tp, _Tp, bool>
    {
      _GLIBCXX14_CONSTEXPR
      bool
      operator()(const _Tp& __x, const _Tp& __y) const
      { return __x > __y; }
    };

  template<typename _Tp>
    struct less : public binary_function<_Tp, _Tp, bool>
    {
      _GLIBCXX14_CONSTEXPR
      bool
      operator()(const _Tp& __x, const _Tp& __y) const
      { return __x < __y; }
    };

再回到刚才的问题,那为什么需绑定器?由于STL接口的限制,有时我们拿到的函数对象和特定STL算法中要接收的函数对象在参数上并不匹配,意思就是需要传递一个一元函数对象,你有一个二元函数对象,那可以通过绑定器提前绑定二元函数对象的其中一个参数,使得最终返回的是一个一元函数对象,那么从二元函数对象到一元函数对象的转换过程,就需要绑定器去实现。

如STL中的泛型算法find_if,可用来查找可变长数组vector中符合某个条件的值(这个条件比如是要大于50,要小于30,要等于25等等)。其第三个参数需要传递一个一元函数对象,假如现在要找到第一个小于70的数,可将绑定器与二元函数对象结合,转换为一元函数对象后传递给find_if

我们知道系统自带的greater<int>()less<int>()模板类对象是二元匿名函数对象,所以需要通过绑定器将其转换为一元函数对象,可以通过bind1stbind2nd去绑定,顾名思义,前者对二元函数对象的第一个参数进行绑定,后者对二元函数对象的第二个参数进行绑定,两个绑定器均返回一元函数对象,用法如下:

sort(vec.begin(), vec.end(), greater<int>()); //从大到小对vector进行排序
find_if(vec.begin(), vec.end(), bind1st(greater<int>(), 70));
find_if(vec.begin(), vec.end(), bind2nd(less<int>(), 70));

两个绑定器分别提前绑定了一个参数,使得二元函数对象+绑定器转换为一元函数对象:

operator()(const T &val)
greater a > b ====> bind1st(greater<int>(), 70) ====> 70 > b
less    a < b ====> bind2nd(less<int>(),    70) ====> a < 70

下面给出bind1st绑定过程图,二元函数对象绑定了第一个数为70,变为一元函数对象,传递给find_if泛型算法,此时find_if所实现的功能就是:找出有序降序数组中第一个小于70的数,所以find_if返回指向65元素的迭代器:

file:///Users/guochen/Notes/docs/media/16656563650484/16657214749366.jpgC++11绑定器bind及function机制

以上就是绑定器的概念。因此需要绑定器的原因就很明显了,绑定器可以返回一个转换后的某元函数对象,用于匹配泛型算法

根据上面的理解,接下来实现一下bind1st,代码实现如下:

/*可以看到 自己实现的绑定器本质上也是个函数对象 调用operator()进行绑定*/
template<typename Compare, typename T>
class _mybind1st {
public:
    _mybind1st(Compare comp, T first) : _comp(comp), _val(first) {}
    bool operator()(const T &second) {
        return _comp(_val, second);
    }
private:
    Compare _comp;
    T _val;
};

/*实现bind1st 函数模板*/
//直接使用函数模板,好处是可以进行类型推演
template<typename Compare, typename T>
_mybind1st<Compare, T> mybind1st(Compare comp, const T &val) { //绑定器返回值_mybind1st为一元函数对象
    return _mybind1st<Compare, T>(comp, val);
}

上述代码中mybind1st绑定器第一个参数Compare comp是要绑定的二元函数对象,第二个参数val是在原有函数对象上绑定的值,最后绑定器调用_mybind1st模板函数对象的小括号运算符重载并返回该一元匿名函数对象,可以看到_mybind1st小括号运算符重载中已将绑定器mybind1st第二个参数val传递给了原本的二元函数对象Compare comp,因此原本绑定器接收的二元函数对象只需要处理第二个参数。所以绑定器返回的函数对象_mybind1st其实是在原本的函数对象上套了一层参数的新的函数对象,阅读上面的代码实现,就可更深刻的理解bind1st的底层原理。

与此同时,不难写出bind2nd的实现,顾名思义该绑定器是对第二个参数进行绑定,不过多赘述,贴出实现代码:

template<typename Compare, typename T>
class _mybind2nd {
public:
    _mybind2nd(Compare comp, T second) : _comp(comp), _val(second) {}
    bool operator()(const T &first) {
        return _comp(first, _val);
    }
private:
    Compare _comp;
    T _val;
};

template<typename Compare, typename T>
_mybind2nd<Compare, T> mybind2nd(Compare comp, const T &val) {
    return _mybind2nd<Compare, T>(comp, val);
}

根据上文,我们清楚了解到泛型算法find_if第三个参数接收一元函数对象,且该泛型算法功能是寻找第一个符合某条件的元素,我们对其补充实现,代码贴出:

/** 
 * 自己实现了find_if后发现其实绑定器返回的就是绑定后的函数对象
 * 使用绑定器的目的:就是将原本某元的函数对象转化为另一个元的函数对象
 * 说白了,绑定器还是对函数对象的一个应用
 **/
template<typename Iterator, typename Compare>
Iterator my_find_if(Iterator first, Iterator last, Compare comp) {
    for(; first != last; ++first) {
        if(comp(*first)) { //调用comp的小括号运算符重载 一元函数对象 comp.operator()(*first)
            return first;
        }
    }
    return last;
}

此时要寻找vector中第一个小于70的数,就可以这样写:

auto it = my_find_if(vec.begin(), vec.end(), mybind1st(greater<int>(), 70));
cout << *it << endl; //打印vec中第一个小于70的数值

以上,围绕bind1stbind2nd以及函数对象等,展开讨论了绑定器bind1stbind2nd的实现原理,但是同时我们也发现其缺点,就是只能对二元函数对象进行绑定转换,让其转换为一元函数对象,那如果遇到很多元的函数对象,我们还得一个一个自己去实现吗?所以将boost库的boost::bind引入到了C++11标准库中,接下来我们介绍C++11的绑定器std::bind,它是对上述两种绑定器的泛化。支持任意函数对象(其实标准库中最多支持29元函数对象,不过这也足够使用了)。

补充:上面都是以函数对象为例,作为绑定器第一个参数传递,其实第一个参数可以是函数对象、成员函数、也可以是普通函数。

总结:绑定器本身是函数模板,绑定器第一个参数可能是普通函数、成员函数或函数对象等,返回的一定是函数对象。还有就是这两个绑定器在C++17中已移除,因此仅用于学习和理解绑定器,也方便我们对C++11引入的bind的学习。至于当前这两个绑定器如何实现对类成员函数的绑定等等我们也没必要去寻找答案了(我一开始也在努力寻找如何使用这两个绑定器去绑定类成员函数,但是发现bind可以很轻松地做到,当然如果大家知道怎么使用bind1stbind2nd绑定类成员函数,也可以评论告知我,感谢~)。

C++11 bind通用绑定器(函数适配器)

我们可将bind函数看作是一个通用的函数适配器,它接受一个可调用函数对象,生成一个新的可调用函数对象来“适应”原对象的参数列表。bind相比于bind1st和bind2nd,实现了“动态生成新的函数”的功能。简言之,可通过bind函数修改原函数并生成一个可以被调用的对象,类似于函数的重载,但是我们又不需要去重新写一个函数,用bind函数就可以实现。相信在上面讲bind1st和bind2nd时,大家对这些关于绑定器(函数适配器)的概念已经有所认知,我们直接看看如何用的吧。

绑定一个普通函数和函数指针

#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
int fun(int a, int b, int c, int d, int e) {
    return a + b - c + d - e;
}
int main() {
   int x = 1, y = 2, z = 3;
   auto g = bind(fun, x, y, _2, z, _1); //第一个参数&可省略 但最好写成&fun
   cout << g(11, 22) << endl; // fun(1, 2, 22, 3, 11) => 1+2-22+3-11
   // cout << bind(fun, x, y, _2, z, _1)(11, 22) << endl; //等价
}

g是有两个参数的二元函数对象,其两个参数分别用占位符placeholders::_2placeholders::_1表示,_2代表二元函数对象的第二个参数22_1代表二元函数对象的第一个参数11。这个新的可调用对象将它自己的参数作为第三个和第五个传递给fun,fun函数的第一个、第二个第四个参数分别被绑定到给定的值xyz上。

绑定一个类的静态成员函数与绑定全局函数没有任何区别,这里不做说明,可参考文章:[ ???? bind绑定器使用方法 ],该文章中bind详细用法中描述了对静态成员方法的使用。

绑定一个类成员函数

绑定器绑定一个成员函数时,我们知道非静态成员函数第一个参数隐藏了一个this指针对象,所以绑定时绑定器第二个参数传递匿名类对象本身。bind和之前的bind1stbind2nd一样,最终返回的一定是函数对象,下面的代码将一个五元函数绑定后,返回了一个三元函数对象,效果等同于调用f.operator()(10, 6, 7)

#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
class Test {
public:
    int func(int a, int b, int c, int d, int e) { return a + b - c + d - e; }
};

int main() {
    auto f = bind(&Test::func, Test(), _1, 12, _3, 5, _2);
    cout << f(10, 6, 7) << endl; //输出:10+12-7+5-6 = 14
    cout << f.operator()(10, 6, 7) << endl;
}

作为类成员函数,需要注意的一点是,如果是非静态的成员函数,它会存在一个默认的this指针,静态的成员函数则不存在this指针,所以在将其作为bind函数的参数时,需要注意使用this指针作为其中一个参数,当使用静态成员函数作为参数时,其用法和全局函数类似,当参数为类内非静态成员函数时,第一个参数必须使用&符号。

注:成为成员函数时,第一个参数之所以必须使用&符号,这部分原因可参考:[ ???? C++中普通函数指针与类成员函数指针的异同 ],文章中有说明具体原因。

以上就是C++11 bind的使用方法,衍生于bind1stbind2nd,支持更多的参数绑定,关于bind函数更多的使用方法,也可参考C++Boost的说明文档:[ ???? Boost.Bind ]。关于bind函数绑定的过程,可参考:[ ???? bind原理图释 ],该文章中的图片方便我们对绑定过程的理解。

C++11 function机制

C++11的function机制是C语言中函数指针的衍生,用来实现回调功能,我们上面的绑定器通常都是以语句执行为单位,当出了某个语句的执行后,绑定器返回的这个函数对象也就随之消失,因此需要有回调功能的function去长期保留绑定器返回的函数对象,以便在需要的时候随时通过function机制调用即可。那有人会问,既然有函数指针,为什么还要再整出来一个function机制?这不是多此一举吗?答案肯定是:很有必要,因为function能做到的,函数指针未必能做到,接下来容我花点篇幅去说明为什么C++中有函数指针还需要std::function

为什么C++中有函数指针还需要std::function?

C/C++中可以使用指针指向一段代码,这个指针就叫函数指针,假设有这样一段代码:

#include <stdio.h>

int func(int a) { return a + 1; }

int main() {
   int (*f)(int) = func;
   printf("%p\n", f);
   return 0;
}

我们定义了一个函数func,然后使用指针变量f指向该函数,然后打印出变量f指向的地址,代码很简单,然后我们编译一下,看下编译后生成的指令,我们重点关注func函数:

int func(int a) {
  4005b6:	55                   	push   %rbp
  4005b7:	48 89 e5             	mov    %rsp,%rbp
  4005ba:	89 7d fc             	mov    %edi,-0x4(%rbp)
  return a + 1;
  4005bd:	8b 45 fc             	mov    -0x4(%rbp),%eax
  4005c0:	83 c0 01             	add    $0x1,%eax
}
  4005c3:	5d                   	pop    %rbp
  4005c4:	c3                   	retq  

可以看到,编译好后的函数func位于地址0x4005b6这个地址,让我们记住这个地址。然后运行一下编译后生成的程序,想一想这段代码会输出什么呢?显然应该是func函数的在内存中的地址!

[root@localhost 07]# ./a.out 
0x4005b6

没有猜错吧,实际上函数指针本质也是一个指针,只不过这个指针指向的不是内存中的一段数据而是内存中的一段代码,就像这样:

C++11绑定器bind及function机制

看到了吧,我们常说的指针一般都是指向内存中的一段数据,而函数指针指向了内存中的一段代码,在这个示例中指向了内存地址0x4005b6,在这个地址中保存了函数func的机器指令。

现在你应该明白函数指针了,细心的同学可能会有一个疑问,为什么编译器在生成可执行文件时就知道函数func存放在内存地址0x4005b6上呢?这不应该是程序被加载到内存后开始运行时才能确定的吗?

函数指针的作用是可以把一段代码当做一个变量传来传去,主要的用途之一就是回调函数。关于回调函数其实是在A模块定义,在B模块被调用,就像这样:

C++11绑定器bind及function机制

然而有时我们会有这样的场景,我们依然需要在模块A定义函数,同时函数A的运行需要依赖B模块产生的数据,然后将模块A定义的函数和模块B产生的数据一并传递给C模块来调用,就像这样:

C++11绑定器bind及function机制

此时,单纯的函数指针已经不够用了,因为函数指针只是单纯的指向了内存中的一段代码,我们不但需要将内存中的一段代码同时也需要将内存中的一块数据传递给模块C,此时你可以定义一个结构体,将代码和数据打包起来,就像这样:

typedef void (*func)(int);

struct functor {
    func f;
    int arg;
};

我们将这个结构体命名为functor,注意看,这个结构中有两部分:

  • 一个指向代码的指针变量
  • 一个保存数据的变量

这样,我们在A模块为指针变量赋值,在B模块为保存数据的变量赋值,然后将此结构体传递给模块C,模块C中可以这样使用:

void run(struct functor func) {
    func->f(func->arg);
}

即,functor既包含了一段代码也包含了这段代码使用的数据,这里的数据也被称为context,即上下文,或者environment,即环境,不管怎么称呼,其实就是函数运行依赖的数据:

C++11绑定器bind及function机制

而这也正是C++中std::function的目的所在。

单纯的函数指针并没有捕捉上下文的能力,这里的上下文就是指代码依赖的数据,你不得不自己动手构造出一个结构体用来存储代码依赖的上下文。在C++中你没有办法单纯的利用函数指针指向对象的成员函数,就是因为函数指针没有办法捕捉this(指向对象的指针)这个上下文。

⚠️注:std::function的作用本质上和我们刚才定义的结构体区别不大。

利用std::function不但可以保存一段代码,同时也可以保存必要的上下文,然后在合适的地方基于上下文调用这段代码


根据上文的介绍,我们也知道std::function相比函数指针的优势所在,要去理解std::function,只需要理解上面提到的结构体即可。接下来我们来谈谈std::function的用法以及一步一步实现一个简单的std::function,剖析其原理。

function的基本用法

接下来直接展示function的直观用法,我们可以把function想象为一个模板类,调用该模板类的operator()()小括号运算符重载,执行封装的函数指针,关于std::function具体实现细节,后续再继续说明,函数指针可用于回调功能,函数对象也可用于回调功能,lambda表达式也可用于回调功能,甚至bind绑定适配后的成员函数也可用于回调功能,那么在不确定的情况下,通过function机制这样的泛型机制统一表示,就会很方便。

普通函数:

void hello() {cout << "hello world!" << endl;}
void hello_str(string str) {cout << str << endl;}
int main() {
    function<void()> func1 = &hello;
    // function<void()> func1(&hello); // 两种调用方法均可
    func1(); //调用func1.operator()() ==> void hello()
    function<void(string)> func2 = &hello_str;
    func2("hello world"); //调用func2.operator()(string) ==> void hello_str(string)
    return 0;
}

模板函数:

template<typename T>
T sum(T a, T b) { return a + b; }
int main() {
    function<int(int, int)> func1 = sum<int>;
    //调用func1.operator()(int, int) ==> int sum(int, int);
    cout << func1(3, 5) << endl; //输出8
    return 0;
}

lambda表达式:

int main() {
    function<int(int, int)> func1 = [](int a, int b)->int { return a + b; };
    cout << func1(3, 5) << endl; //打印8 调用func1.operator()(int, int) ==> 调用lambda表达式返回求和结果
    return 0;
}

函数对象:

class PrintAdd1 {
public:
    void operator()(int left, int right) {
        cout << "sum : " << left + right << endl;
    }
};
int main() {
    function<void(int, int)> func1 = PrintAdd1(); //调用默认无参构造函数创建匿名类对象给func1
    func1(3, 5); //func1.operator()(int, int) ==> 调用void PrintAdd1.operator(int, int)
    return 0;
}

模板函数对象:

template<typename T>
class PrintAdd2 {
public:
    void operator()(T left, T right) {
        cout << "sum : " << left + right << endl;
    }
};
int main() {
    function<void(int, int)> func1 = PrintAdd2<int>(); //调用默认无参构造函数创建匿名模板类对象给func1
    func1(3, 5); //func1.operator()(int, int) ==> 调用void PrintAdd2.operator()(int, int)
    return 0;
}

类静态成员函数:

class StaticClass1 {
public:
    static void hello_static(string s) {
        cout << s << endl;
    }
};
int main() {
    function<void(string)> func1 = &StaticClass1::hello_static;
    func1("hello world"); //func1.operator()(string) ==> 调用void hello_static(string)
    return 0;
}

模板类静态成员函数:

template<typename T>
class StaticClass2 {
public:
    static void hello_static(T out) {
        cout << out << endl;
    }
};
int main() {
    function<void(string)> func1 = &StaticClass2<string>::hello_static;
    func1("static.. hello world"); //func1.operator()(string) ==> 调用void StaticClass2<string>::hello_static(string)
    return 0;
}

普通类成员函数:

class Test {
public:
    void hello(string str) {
        cout << str << endl;
    }
};
int main() {
    // function<void(Test *, string)> func = &Test::hello;
    // func(&Test(), "call Test::hello"); //这种第一个参数传递匿名对象的方法在GCC8.4下不可行 在vs2017下可行 不建议使用匿名对象
    Test test; //定义对象
    function<void(Test *, string)> func1 = &Test::hello;
    func1(&test, "call Test::hello"); //func1.operator(Test *, string) ==> 调用void Test::hello(string)
    return 0;
}

模板类成员函数:

template<typename T>
class Test2 {
public:
    void hello(T str) {
        cout << str << endl;
    }
};
int main() {
    Test2<string> test2;
    function<void(Test2<string> *, string)> func1 = &Test2<string>::hello;
    func1(&test2, "call template Test::hello"); //func1.operator(Test2 *, string) ==> 调用void Test2<string>::hello(string)
    return 0;
}

function底层原理剖析

function用法有基本了解后,为了剖析function底层原理,我们还需知道模板的「特例化」以及「可变参数模板』,这里不再说明,可参考我以下两篇博文,已经对模板特化和可变参数模板进行了解释:

function是C++11特有的一种比函数指针更灵活的机制,现在如果我们要接收一个hello函数,形如:

void hello(string str) { cout << str << endl; }

该如何实现呢?function利用了函数对象的手段,结合函数指针去调用小括号运算符重载实现,因此理所应当的实现是这样的,内部有一个函数指针_pfunc,并且该函数指针在operator()小括号运算符重载重被调用:

//前向声明模板类
template<typename T>
class myfunction {};

//单个类型参数模板完全特例化
template<typename R, typename A1>
class myfunction<R(A1)> {
public:
    using PFUNC = R (*)(A1);
public:
    myfunction(PFUNC pfunc) : _pfunc(pfunc) {}
    R operator()(A1 arg) {
        return _pfunc(arg);
    }
private:
    PFUNC _pfunc;
};

function对象接收hello函数时,R作为返回值会被推导为voidA1作为单一参数类型被推导为string类型:

myfunction<void(string)> f1 = &hello;
f1("hello world");  //打印"hello world"

那现在如果我们要接收两个参数的sum求和函数呢?

int sum(int a, int b) { return a + b; }

其实理解了function的原理后,这个时候要接收一个双参数sum求和函数,可以再去特化一个适合sum的类,其中R推导为intA1sum的第一个参数类型intA2sum的第二个类型int

#include <iostream>
using namespace std;

int sum(int a, int b) { return a + b; }

template<typename T>
class myfunction {};

//两个类型参数模板完全特例化
template<typename R, typename A1, typename A2>
class myfunction<R(A1, A2)> {
public:
    using PFUNC = R (*)(A1, A2);
public:
    myfunction(PFUNC pfunc) : _pfunc(pfunc) {}
    R operator()(A1 arg1, A2 arg2) {
        return _pfunc(arg1, arg2);
    }
private:
    PFUNC _pfunc;
};

int main() {
    myfunction<int(int, int)> f2 = &sum;
    // myfunction<int(int, int)> f2(&sum);
    cout << f2(3, 4) << endl;   // 输出5
    return 0;
}

以上就是function去接收单参数和双参数时的实现,但是这有个很致命的缺点,如果要接收三个参数、四个参数、十个参数、一百个参数、一千个参数呢?(当然这不太可能,就是单纯去考虑这种情况),那是不是还得对不同参数都进行一次实现?那模板的意义何在?如何消除代码冗余就是问题所在,我们需要用到可变模板参数,我的这篇文章已经说明什么是可变参数模板,如何去使用它:[ ???? 泛化之美 —— C++11 可变参数模板的妙用 ]

所以通过可变模板参数,我们去实现一个可变参数的function,该function可以接收任意数量参数的函数、函数对象、lambda表达式、bind适配后的成员函数和普通函数等等:

template<typename T>
class myfunction {};

/*那如果类型参数多了,我们还要一个一个定义吗??其实可以采用变长参数模板实现*/
// 可变参数模板完全特例化
template<typename R, typename... A>
class myfunction<R(A...)> {
public:
    using PFUNC = R (*)(A...);
public:
    myfunction(PFUNC pfunc) : _pfunc(pfunc) {}
    R operator()(A... arg) {
        return _pfunc(arg...);
    }
private:
    PFUNC _pfunc;
};

根据以上实现,我们对function的底层原理有了深刻的认识,能够自己实现一个接收任意参数的function模板类。虽然以上实现和标准库还差得很远,但是起到了学习、理解的作用吧。

总结

本文通过C++11之前的bind1stbind2nd引入,介绍了C++11的bind的使用,理清了它的作用,何谓“绑定”?然后介绍了function的使用方法,并对其进行了逻辑上的实现,以上这些知识点都挺难的,涉及函数对象、模板特化、可变参数模板。但是也算是一次系统性总结吧,感觉意义蛮大的。C++11的路还很远,以后有机会再继续深耕吧。

顺便记录一下,我还在为自己的秋招迷茫中,如果有好消息,我在回来还愿!