可调用对象包装器, 绑定器
可调用对象
可调用对象是指在 C++ 中能够像函数一样被调用的实体。它包括了多种类型的对象,使得它们能够像函数一样被调用,可以是函数、函数指针、函数对象、Lambda 表达式等。在C++中,具有以下特征之一的实体都被认为是可调用对象:
1.函数: 常规的函数,包括全局函数和类的成员函数。
cpp
void regularFunction(int x) {
// ...
}
2.函数指针: 指向函数的指针。
cpp
#include <iostream>
// 函数原型
void sayHello(const char* name) {
std::cout << "Hello, " << name << "!" << std::endl;
}
int main() {
// 定义函数指针并初始化为指向 sayHello 函数
void (*helloFunctionPointer)(const char*) = sayHello;
// 使用函数指针调用函数
helloFunctionPointer("Alice");
helloFunctionPointer("Bob");
return 0;
}
3.函数对象: 重载了函数调用运算符 operator()
的类。(仿函数)
cpp
#include <iostream>
// 函数对象类
struct Functor {
int operator()(int x, int y) {
return x + y;
}
};
int main() {
// 创建函数对象的实例
Functor adder;
// 使用函数对象
int result = adder(3, 4);
std::cout << "Result: " << result << std::endl;
return 0;
}
4.Lambda表达式: 匿名的函数对象。
cpp
#include <iostream>
int main() {
// Lambda 表达式,接受一个整数参数并返回它的平方
auto square = [](int x) {
return x * x;
};
// 调用 Lambda 表达式
int result = square(5);
std::cout << "Square of 5 is: " << result << std::endl;
return 0;
}
5.类的成员函数指针: 指向类的成员函数或对象的指针。
cpp
#include <iostream>
#include <string>
#include <vector>
using namespace std;
struct Test
{
void print(int a, string b)
{
cout << "name: " << b << ", age: " << a << endl;
}
int m_num;
};
int main(void)
{
// 定义类成员函数指针指向类成员函数
void (Test::*func_ptr)(int, string) = &Test::print;
// 类成员指针指向类成员变量
int Test::*obj_ptr = &Test::m_num;
Test t;
// 通过类成员函数指针调用类成员函数
(t.*func_ptr)(19, "Monkey D. Luffy");
// 通过类成员指针初始化类成员变量
t.*obj_ptr = 1;
cout << "number is: " << t.m_num << endl;
return 0;
}
在上面的例子中满足条件的这些可调用对象对应的类型被统称为可调用类型。C++中的可调用类型虽然具有比较统一的操作形式,但定义方式五花八门,这样在我们试图使用统一的方式保存,或者传递一个可调用对象时会十分繁琐。现在,C++11通过提供std::function 和 std::bind统一了可调用对象的各种操作。
可调用对象包装器
std::function是可调用对象的包装器。它是一个类模板,可以容纳除了类成员(函数)指针之外的所有可调用对象。通过指定它的模板参数,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行它们。
基本用法
std::function必须要包含一个叫做functional的头文件,可调用对象包装器使用语法如下:
cpp
#include <functional>
std::function<返回值类型(参数类型列表)> diy_name = 可调用对象;
下面的实例代码中演示了可调用对象包装器的基本使用方法:
cpp
#include <iostream>
#include <functional>
using namespace std;
int add(int a, int b)
{
cout << a << " + " << b << " = " << a + b << endl;
return a + b;
}
class T1
{
public:
static int sub(int a, int b)
{
cout << a << " - " << b << " = " << a - b << endl;
return a - b;
}
};
class T2
{
public:
int operator()(int a, int b)
{
cout << a << " * " << b << " = " << a * b << endl;
return a * b;
}
};
int main(void)
{
// 绑定一个普通函数
function<int(int, int)> f1 = add;
// 绑定以静态类成员函数
function<int(int, int)> f2 = T1::sub;
// 绑定一个仿函数
T2 t;
function<int(int, int)> f3 = t;
// 函数调用
f1(9, 3);
f2(9, 3);
f3(9, 3);
return 0;
}
输入结果如下:
cpp
9 + 3 = 12
9 - 3 = 6
9 * 3 = 27
通过测试代码可以得到结论:std::function可以将可调用对象进行包装,得到一个统一的格式,包装完成得到的对象相当于一个函数指针,和函数指针的使用方式相同,通过包装器对象就可以完成对包装的函数的调用了。
作为回调函数使用
因为回调函数本身就是通过函数指针实现的,使用对象包装器可以取代函数指针的作用,来看一下下面的例子:
cpp
#include <iostream>
#include <functional>
using namespace std;
// 模拟某个异步操作
void performAsyncOperation(function<void()> callback) {
// 模拟异步操作完成后调用回调函数
std::cout << "Async operation completed." << std::endl;
callback();
}
// 回调函数1
void callback1() {
std::cout << "Callback 1 called." << std::endl;
}
// 回调函数2
void callback2() {
std::cout << "Callback 2 called." << std::endl;
}
int main() {
// 使用 function 作为回调函数的容器
function<void()> callbackFunc1 = callback1;
function<void()> callbackFunc2 = callback2;
// 执行异步操作,传递回调函数
performAsyncOperation(callbackFunc1);
// 执行异步操作,传递另一个回调函数
performAsyncOperation(callbackFunc2);
return 0;
}
在这个例子中,CallbackFunction
是一个 std::function
类型,用于表示无返回值且无参数的回调函数。performAsyncOperation
函数模拟了一个异步操作,完成后调用传递的回调函数。在 main
函数中,创建了两个不同的回调函数,然后分别传递给 performAsyncOperation
函数。
绑定器
std::bind用来将可调用对象与其参数一起进行绑定。绑定后的结果可以使用std::function进行保存,并延迟调用到任何我们需要的时候。通俗来讲,它主要有两大作用:
- 将可调用对象与其参数一起绑定成一个仿函数。
- 将多元(参数个数为n,n>1)可调用对象转换为一元或者(n-1)元可调用对象,即只绑定部分参数。
绑定器函数使用语法格式如下:
cpp
// 绑定非类成员函数/变量
auto f = std::bind(可调用对象地址, 绑定的参数/占位符);
// 绑定类成员函/变量
auto f = std::bind(类函数/成员地址, 类实例对象地址, 绑定的参数/占位符);
先来看两个简单例子:
cpp
#include <functional>
#include <iostream>
void functionToBind(int a, int b) {
std::cout << "Sum: " << a + b << std::endl;
}
int main() {
// 使用 std::bind 绑定函数和参数
auto boundFunction = std::bind(functionToBind, 10, std::placeholders::_1);
// 调用绑定的函数
boundFunction(20); // 输出 "Sum: 30"
return 0;
}
在这个例子中,std::bind
绑定了 functionToBind
函数,并将第一个参数绑定为 10
,第二个参数使用 std::placeholders::_1
占位符,表示稍后提供的参数 。然后,通过调用 boundFunction(20)
,实际提供了一个参数 20
,而 std::placeholders::_1
被替换为 20
,最终调用了 functionToBind(10, 20)
。
cpp
#include <functional>
#include <iostream>
class MyClass {
public:
void memberFunction(int a, int b) {
std::cout << "Sum: " << a + b << std::endl;
}
};
int main() {
MyClass obj;
// 使用 std::bind 绑定成员函数和对象
auto boundMemberFunction = std::bind(&MyClass::memberFunction, &obj, 10, std::placeholders::_1);
// 调用绑定的成员函数
boundMemberFunction(20); // 输出 "Sum: 30"
return 0;
}
这个例子演示了如何使用 std::bind
绑定类的成员函数。注意,需要提供对象的地址作为第一个参数 , &obj
为第二个参数。后续的参数和占位符的使用方式与前面的例子类似。
在上面的程序中,使用了std::bind绑定器,在函数外部通过绑定不同的函数,控制了最后执行的结果。std::bind绑定器返回的是一个仿函数类型,得到的返回值可以直接赋值给一个std::function,在使用的时候我们并不需要关心绑定器的返回值类型,使用auto进行自动类型推导就可以了。
placeholders::_1是一个占位符,代表这个位置将在函数调用时被传入的第一个参数所替代。同样还有其他的占位符placeholders::_2、placeholders::_3、placeholders::_4、placeholders::_5等......
有了占位符的概念之后,使得std::bind的使用变得非常灵活:
cpp
#include <iostream>
#include <functional>
using namespace std;
void output(int x, int y)
{
cout << x << " " << y << endl;
}
int main(void)
{
// 使用绑定器绑定可调用对象和参数, 并调用得到的仿函数
bind(output, 1, 2)();
bind(output, placeholders::_1, 2)(10);
bind(output, 2, placeholders::_1)(10);
// error, 调用时没有第二个参数
// bind(output, 2, placeholders::_2)(10);
// 调用时第一个参数10被吞掉了,没有被使用
bind(output, 2, placeholders::_2)(10, 20);
function<void(int, int)> func1 = bind(output, placeholders::_1, placeholders::_2);
auto func2 = bind(output, placeholders::_2, placeholders::_1);
func1(10, 20);
func2(10, 20);
return 0;
}
示例代码执行的结果:
cpp
1 2 // bind(output, 1, 2)();
10 2 // bind(output, placeholders::_1, 2)(10);
2 10 // bind(output, 2, placeholders::_1)(10);
2 20 // bind(output, 2, placeholders::_2)(10, 20);
10 20 // bind(output, placeholders::_1, placeholders::_2)(10, 20);
20 10 // bind(output, placeholders::_2, placeholders::_1)(10, 20);
通过测试可以看到,std::bind可以直接绑定函数的所有参数,也可以仅绑定部分参数。在绑定部分参数的时候,通过使用std::placeholders来决定空位参数将会属于调用发生时的第几个参数。
可调用对象包装器std::function是不能实现对类成员函数指针或者类成员指针的包装的,但是通过绑定器std::bind的配合之后,就可以完美的解决这个问题了,再来看一个例子,然后再解释里边的细节:
cpp
#include <iostream>
#include <functional>
using namespace std;
class Test
{
public:
void output(int x, int y)
{
cout << "x: " << x << ", y: " << y << endl;
}
int m_number = 100;
};
int main(void)
{
Test t;
// 绑定类成员函数
function<void(int, int)> f1 =
bind(&Test::output, &t, placeholders::_1, placeholders::_2);
// 绑定类成员变量(公共)
function<int&(void)> f2 = bind(&Test::m_number, &t);
// 调用
f1(520, 1314);
f2() = 2333;
cout << "t.m_number: " << t.m_number << endl;
return 0;
}
示例代码输出的结果:
cpp
x: 520, y: 1314
t.m_number: 2333
在用绑定器绑定类成员函数或者成员变量的时候需要将它们所属的实例对象一并传递到绑定器函数内部。f1的类型是function<void(int, int)>,通过使用std::bind将Test的成员函数output的地址和对象t绑定,并转化为一个仿函数并存储到对象f1中。
使用绑定器绑定的类成员变量m_number得到的仿函数被存储到了类型为function<int&(void)>的包装器对象f2中,并且可以在需要的时候修改这个成员。其中int是绑定的类成员的类型,并且允许修改绑定的变量,因此需要指定为变量的引用,由于没有参数因此参数列表指定为void。
示例程序中是使用function包装器保存了bind返回的仿函数,如果不知道包装器的模板类型如何指定,可以直接使用auto进行类型的自动推导,这样使用起来会更容易一些。