持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第15天,点击查看活动详情
概念
bind
函数是一个通用的函数适配器,正如其名,是用来绑定的,绑定可调用对象与其参数,实际上是一种延迟计算的思想,可以绑定普通函数,指针函数,lambda 表达式以及类的成员函数,将调用状态(主要指的是传入的参数)保存起来,建立一个可随时调用的对象(类似上篇文章讲述的std::function),以便后续在任何时候执行。
bind
函数接受一个可调用对象,生成一个新的可调用对象来适配原对象。std::bind() 函数的参数可以在绑定的时候传入,也可以放置一个参数占位符,在实际调用执行的时候传入,参数占位符定义在命名空间 std::placeholders 中,第N个参数占位符书写为 std::placeholders::_N,相当于定义了执行对象在调用的时候必须传入N个参数,N必须依次递增,简单示例:
函数原型
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
template< class R, class Fn, class... Args >
/* unspecified */ bind( Fn&& fn, Args&&... args );
与 std::function 不同的是,function 是模板类,bind 是模板函数,而 bind 返回的可调用对象可以直接给 function 进行包装并保存。
当用作普通函数的绑定时,第一个参数是可调用对象(普通函数、lambda等),而第二个参数开始对应可调用对象的参数表。std::placeholders::_1 代表可调用对象的第一个参数,_2就代表第二个参数,依此类推。
当用作类成员函数的绑定时,第一个参数仍然是作为类成员的可调用对象引用,第二个参数则是对象的指针,而第三个参数开始对应可调用对象的参数表。同样使用 std::placeholders::_N 依次向后推。
std::bind主要有以下两个作用:
- 将可调用对象和其参数绑定成一个仿函数;
- 只绑定部分参数,减少可调用对象传入的参数。
用法
调用指向非静态成员函数指针或指向非静态数据成员指针时,第一个参数必须是引用或指针(可以包含智能指针,如 std::shared_ptr 与 std::unique_ptr),指向将访问其成员的对象。
#include <functional>
#include <iostream>
#include <memory>
void f(int n1, int n2, int n3, const int& n4, int n5) {
std::cout << n1 << ' ' << n2 << ' '
<< n3 << ' ' << n4 << ' ' << n5 << '\n';
}
int g(int n1) { return n1; }
struct Foo {
void print_sum(int n1, int n2) { std::cout << n1 + n2 << '\n'; }
int data = 10;
static void print_static(int n1, int n2) { std::cout << n1 + n2 << '\n'; }
void operator()(int n1, int n2) { std::cout << n1 + n2 << '\n'; }
};
int main() {
using namespace std::placeholders; // 对于 _1, _2, _3...
// 演示参数重排序和按引用传递
int n = 7;
// _1 与 _2 来自 std::placeholders ,并表示将来会传递给 f1 的参数
auto f1 = std::bind(f, _2, 42, _1, std::cref(n), n);
n = 10;
f1(1, 2, 1001); // 1 为 _1 所绑定, 2 为 _2 所绑定,不使用 1001
// 进行到 f(2, 42, 1, n, 7) 的调用
// 嵌套 bind 子表达式共享占位符
auto f2 = std::bind(f, _3, std::bind(g, _3), _3, 4, 5);
f2(10, 11, 12); // 进行到 f(12, g(12), 12, 4, 5); 的调用
// 绑定指向成员函数指针
Foo foo;
auto f3 = std::bind(&Foo::print_sum, &foo, 95, _1);
f3(5);
// 绑定指向数据成员指针
auto f4 = std::bind(&Foo::data, _1);
std::cout << f4(foo) << '\n';
// 智能指针亦能用于调用被引用对象的成员
std::cout << f4(std::make_shared<Foo>(foo)) << '\n'
<< f4(std::make_unique<Foo>(foo)) << '\n';
// 也可以这样绑定指向数据成员指针
auto f4_1 = std::bind(&Foo::data, &foo);
std::cout << f4_1() << '\n';
// 修改数据成员
f4_1() = 20;
std::cout << foo.data << '\n';
// 绑定指向静态成员函数指针
auto f5 = std::bind(&Foo::print_static, 95, _1);
f5(5);
// 绑定对象函数
auto f6 = std::bind(Foo(), 95, _1);
f6(5);
}
简单对上面的程序解读一下,首先上面的f1中使用到了_2
和_1
两个占位符,也就是在调用f1时对应它传入的第二个和第一个实参分别对应放到该位置,所以当调用f1(1, 2, 1001);
时,实参1对应放到了_1
占位符的位置,也就是真正函数f的第三个参数,实参2对应放到了_2
占位符的位置,对象f函数的第一个参数,最终调用的f函数是f(2, 42, 1, n, 7)
。
对于f3,bind绑定类成员函数时,第一个参数表示对象的成员函数的指针,第二个参数表示对象的地址。必须显示的指定&Foo::print_sum
,因为编译器不会将对象的成员函数隐式转换成函数指针,所以必须在成员函数前添加&
取地址符号。使用对象成员函数的指针时,必须要知道该指针属于哪个对象,因此第二个参数为对象的地址,如&foo
,也是需要使用取地址符显示指定。
f4,绑定类成员变量,可以通过函数调用的方式访问和修改它,但是可以修改的前提是,可调用对象封装器的函数返回值必须是引用类型,如下,f4的类型的返回值就是引用类型,所以它支持修改。
int& std::_Bind<int Foo::* (Foo*)>::operator()<, int&>();
f5,绑定类静态函数。
f6,绑定类函数对象。
绑定引用参数
另外,bind
函数中非占位符的参数,默认会以值拷贝的方式传递给返回的可调用对象中,所以直接使用bind
,无法将参数以引用方式传递,或是绑定的参数类型无法拷贝。我们需要使用ref
函数(函数ref
返回一个对象,包含给定的引用,此对象是可以拷贝的)来实现以引用方式传递参数,或将无法拷贝的参数类型可拷贝。
#include <functional>
#include <iostream>
void add(int &x, int &y) {
x++;
y++;
}
int main()
{
int x = 1, y = 1;
//x以引用方式传递,y以值方式传递
auto func = std::bind(add, std::ref(x), y);
func();
std::cout << x << " " << y; //结果 2 1
return 0;
}
总结
bind的思想实际上是一种延迟计算的思想,将可调用对象保存起来,然后在需要的时候再调用。而且这种绑定是非常灵活的,不论是普通函数、函数对象、还是成员函数都可以绑定,而且其参数可以支持占位符,比如你可以这样绑定一个二元函数auto f = bind(&func, _1, _2);
,调用的时候通过f(1,2)实现调用。
在实际使用过程中,其实lambda表达式使用起来更简单直观,觉得lambda表达式在绝大多数情况下可以替代bind。
参考