目录
本文说明:
在Linux后期中,经常会用到bind,但bind这东西是在C++的中后期学的,导致博主总是想不起来bind的用法是什么,所以特此为bind写一篇使用说明,以后遇到bind就可以来这温习一下~~
一:bind的意义
std::bind
是一个强大的函数适配器,它位于 <functional>
头文件中。它的主要作用是将一个可调用对象(函数、函数指针、成员函数、函数对象、Lambda 表达式)与其参数进行绑定,生成一个新的可调用对象。一般用来减少传参或者调换参数顺序!
Q1:怎么理解bind的适配?
A1:就像电源适配器(充电头)一样,它能把墙上的交流电(一种形式)转换成手机需要的直流电(另一种形式)。std::bind 也是干这个的——它转换一个函数的形式。那怎么适配?很简单,bind它通过“固定”一些参数,把一个需要很多参数的函数,变成一个新函数。这个新函数需要的参数更少,或者参数的顺序变了,就达到了适配的效果
Q2:可调用对象是什么?
A2:这就是指任何能像函数一样后面加个 ()
来调用的东西。它不只是普通的函数,还包括:
-
普通函数:
printf
,myFunction
-
函数指针:
void (*fp)() = &myFunction;
-
成员函数:
MyClass::memberFunction
(需要和对象一起用) -
函数对象 (Functors): 一个重载了
operator()
的类,看起来像函数。 -
Lambda 表达式:
[]() { /* ... */ }
Q3:生成一个新的可调用对象是什么意思?
A3:因为bind之后,函数的形式已经改变了,所以bind函数的返回值就是一个新的、行为被改变了的函数。你可以把它存到一个变量里(通常用 auto
),然后像调用普通函数一样调用它。
bind意义总结:
想象一下,你要做一道菜,比如鱼香肉丝。正常的做法是:每次做的时候,你都要准备好肉丝、木耳、胡萝卜丝、调料等一系列食材,然后下锅炒。
std::bind
的作用就是:让你可以提前把一部分食材准备好,封装成一个“料理包”。下次你想吃的时候,只需要完成最后一步(比如下锅炒一下)就行了。
二:bind的用法
bind的第一个参数,必须填写你绑定的那个函数!
1:第一个参数
①:普通函数(函数指针)
void printSum(int a, int b) { std::cout << a + b; }
// 第一个参数是函数指针
auto boundFunc = std::bind(printSum, ...);
②:函数对象(Functor,重载了 operator()
的类)
struct Adder {
int operator()(int a, int b) const { return a + b; }
};
Adder adder;
// 第一个参数是函数对象实例
auto boundFunc = std::bind(adder, ...);
③:Lambda 表达式
auto lambda = [](int x) { return x * x; };
// 第一个参数是Lambda表达式
auto boundFunc = std::bind(lambda, ...);
④:成员函数(Member Function Pointer)
class Logger {
public:
void log(const std::string& msg) { std::cout << msg; }
};
Logger logger;
// 第一个参数是成员函数指针 &Logger::log
auto boundFunc = std::bind(&Logger::log, ...);
总结:
填写函数名,本质就是得到函数的地址,一般的函数名就等于与&函数名,但是成员函数不可以,成员函数没有这种隐式转化,所以要想绑定一个成员函数,必须进行&操作!这就是④与其他的区别!
2:其余参数
其余参数一般使用占位符来填写,用几个,则根据你想怎么设计bind之后形成的新函数!
占位符就是:std::placeholders::_1 ,std::placeholders::_2,std::placeholders::_3这种,当然你也可以写作_1,_2,_3这种,想使用后者需要展开命名空间:using namespace std::placeholders;
当然,我觉得使用std::placeholders::_1时最好的行为,因为在任何时候都不会发生命名冲突!
而bind减少传参或者调换参数顺序,就是这几个占位符起到的作用,
注:减少传承的参数也叫作绑定部分参数,更换参数的顺序也叫作重新排列参数顺序
举个例子:
a:重排参数顺序
我们的被绑定的函数printInfo是参数顺序是名字,年龄,分数,打印的语句也是如此,下面我们用bind更改这个函数的参数的顺序:
#include <iostream>
#include <functional>
using namespace std::placeholders; // 引入 _1, _2, _3
void printInfo(const std::string& name, int age, double score) {
std::cout << name << " is " << age << " years old, score: " << score << std::endl;
}
int main() {
// 原函数顺序: (name, age, score)
// 1. 完全绑定:不需要占位符
auto f1 = std::bind(printInfo, "Alice", 25, 99.5);
f1(); // 输出: Alice is 25 years old, score: 99.5
// 2. 使用占位符:改变参数顺序
// 新函数要求参数顺序为: (分数, 年龄, 名字)
auto f2 = std::bind(printInfo, _3, _2, _1); // _1<-score, _2<-age, _3<-name
f2(88.5, 30, "Bob"); // 输出: Bob is 30 years old, score: 88.5
return 0;
}
解释:
①:第一次使用bind我们没有对这个函数做任何事情,无法就是我们bind之后可以用f1函数达到和printInfo函数一样的效果
②:第二次使用bind我们用占位符更改了顺序,_3,_2,_1意味着,我们用这个函数传参的时候,比如f2(88.5, 30, "Bob");,此时的88.5明明是第一个参数,但是由于_1在最后一个参数,所以88.5会传入到score参数中,以此类推,所以占位符的数字就像是一个归属地,_1则代表第一个参数传入到_1,_2,_3也是如此
b:绑定部分参数
依旧是上面的例子,我们现在把某个参数写死,也就是本该填写占位符的参数换成了具体的参数,就能到达绑定部分参数的例子:
#include <iostream>
#include <functional>
using namespace std::placeholders; // 引入 _1, _2, _3
void printInfo(const std::string& name, int age, double score) {
std::cout << name << " is " << age << " years old, score: " << score << std::endl;
}
int main() {
// 原函数顺序: (name, age, score)
// 3. 混合使用:固定某些参数,重排其他参数
// 固定名字为"Charlie",新函数参数顺序为: (年龄, 分数)
auto f3 = std::bind(printInfo, "Wtt", _1, _2); // _1<-score, _2<-age
f3(22, 100); // 输出: Charlie is 22 years old, score: 95
return 0;
}
运行结果:
解释:我们将参数写死为"Wtt"之后,后面我们就不用传名字这个参数了,只用传后两个,而占位符的顺序即使调换,也只会影响到后两个参数,和名字参数无关!
c:混合使用
在b的基础上,我们再调换一下顺序:
#include <iostream>
#include <functional>
using namespace std::placeholders; // 引入 _1, _2, _3
void printInfo(const std::string& name, int age, double score) {
std::cout << name << " is " << age << " years old, score: " << score << std::endl;
}
int main() {
// 原函数顺序: (name, age, score)
// 3. 混合使用:固定某些参数,重排其他参数
// 固定名字为"Charlie",新函数参数顺序为: (年龄, 分数)
auto f3 = std::bind(printInfo, "Wtt", _2, _1); // _1<-score, _2<-age
f3(22, 100); // 输出: Charlie is 22 years old, score: 95
return 0;
}
解释:符合预期,调换占位符只会影响到没有写死的参数
三:bind绑定类的成员函数
bind中最常用的,最难的就去绑定一个类的成员函数,在第二大点的1小点中,我们知道只要绑定类的成员函数,则第一个参数必须&
此外,类的成员函数的第一个参数都是隐式的this指针,类似是this
指针,ClassName*,所以我们想绑定一个类的成员函数的时候,我们的bind的第二个参数也必须写死,而不能使用占位符!
而bind去绑定类的成员函数的第二个参数的写法有几种:
我们就用一个简单的 Logger
类作为例子,想要bind绑定其中的log函数!
class Logger {
public:
void log(const std::string& message) {
std::cout << "Log: " << message << std::endl;
count++;
}
int count = 0;
};
1:传递对象指针 (&obj
)
int main()
{
Logger logger;
auto func1 = std::bind(&Logger::log, &logger, _1);
// 成员函数地址 |对象指针|
// ^
// 第二个参数
func1("Hello"); // 正确:调用 logger.log("Hello")
}
特点:
-
最常用的方式。
-
要求
logger
对象必须持续存在,直到func1
不再被使用。 -
不会拷贝
Logger
对象。
2:传递对象副本 (obj
)
int main()
{
Logger logger;
auto func2 = std::bind(&Logger::log, logger, _1);
// |对象副本|
func2("Test"); // 注意:调用的是副本的 log 方法!
}
特点:
-
会调用
Logger
的拷贝构造函数,创建对象的完整副本。 -
操作的是副本,对原对象
logger
没有任何影响。 -
通常这是错误用法,除非你确实需要操作副本。
-
如果类不可拷贝(如含有
unique_ptr
成员),会编译错误。
3:使用占位符延迟绑定 (_1
)
也可以不急着传bind的第二个参数,使用占位符,但是这种方法不常用
int main()
{
Logger logger1, logger2;
auto func3 = std::bind(&Logger::log, _1, _2);
// |对象占位符| |参数占位符|
// 调用时需要同时提供Logger对象和消息
func3(&logger1, "From logger1"); // 调用 logger1.log("From logger1")
func3(&logger2, "From logger2"); // 调用 logger2.log("From logger2")
}
特点:
-
极其灵活,可以动态决定对哪个对象操作。
-
创建的是"通用"的绑定器。
-
参数顺序:
_1
对应对象,_2
对应成员函数的第一个参数,以此类推。
4:传递对象引用
写法:使用 std::ref
包装对象
语义:按引用绑定,同样要求对象生命周期有效。
int main()
{
Logger logger;
auto func2 = std::bind(&Logger::log, std::ref(logger), _1);
// |引用包装器|
func2("World"); // 正确:调用 logger.log("World")
}
特点:
-
与传递指针效果类似,都是操作原始对象。
-
在模板代码或需要统一接口时很有用。
-
明确表示"我要按引用传递"的意图。
解释:当你的bind的第二个参数想传递对象引用的时候,必须使用ret引用包装器,切记不要std::bind(func, my_object&);,这是常见错误写法,&引用是类型修饰符,你不能修饰变量
四:bind的终极用法
bind在Linux中,用于形成回调函数是其最强大和最实用的应用场景!
类A中想使用类B的方法,所以类A的构造函数的参数就需要接收类B的方法,但是呢,我们可以用bind来把类A的构造函数中的用来接收类B的方法的参数写死,这样我们就不需要在麻烦的一次次传递参数了,其次类A中已经会写好了接收到类B的函数后,会怎么使用的代码!
简述如下:
-
类A 需要调用 类B 的方法
-
通过构造函数注入依赖(类B的方法)
-
使用
std::bind
"写死"(固化)参数,简化调用 -
类A内部已经实现了如何使用这个注入的方法
具体例子如下:
#include <iostream>
#include <functional>
class Logger {
public:
void log(const std::string& message, int severity) {
std::cout << "[" << severity << "] " << message << std::endl;
}
};
class Processor {
private:
std::function<void(const std::string&, int)> logCallback;
public:
// 构造函数接收一个复杂的回调函数
Processor(std::function<void(const std::string&, int)> callback)
: logCallback(callback) {}
void process() {
// 每次调用都需要传递所有参数
logCallback("Processing started", 1);
logCallback("Working...", 2);
logCallback("Processing completed", 1);
}
};
int main() {
Logger logger;
//使用bind绑定
Processor processor(std::bind(&Logger::log, &logger, std::placeholders::_1, std::placeholders::_2));
processor.process();
return 0;
}
运行结果:
解释:我们对Processor的构造函数的参数进行了bind,这样我们之后创建Processor对象的时候,再也不需要手动的传递类logger的成员函数了!
分解解释
1.
std::bind(&Logger::log, ...)
&Logger::log
:获取Logger
类的log
成员函数的地址这是我们要绑定的目标函数
2.
&logger
第二个参数是调用成员函数所需的对象实例指针
std::bind
需要知道在哪个对象上调用这个成员函数3.
std::placeholders::_1
和std::placeholders::_2
_1
:表示第一个占位符,对应未来的第一个参数(msg
)
_2
:表示第二个占位符,对应未来的第二个参数(severity
)这些占位符定义了参数如何传递给被绑定的函数
除开bind方法,不管你用哪种,你都不可能在Processor类中可以直接回调logCallback函数!