在 C++11 中,可调用对象包装器和绑定器是两个非常实用的特性,它们增强了 C++ 在处理可调用对象(如函数、函数指针、成员函数指针、lambda 表达式等)时的灵活性和可复用性。
1. 可调用对象
在C++中存在“可调用对象”这么一个概念。准确来说,可调用对象有如下几种定义:
- 是一个函数指针
int print(int a, double b)
{
cout << a << b << endl;
return 0;
}
// 定义函数指针
int (*func)(int, double) = &print;
- 是一个具有operator()成员函数的类对象(仿函数)
#include <iostream>
#include <string>
#include <vector>
using namespace std;
struct Test
{
// ()操作符重载
void operator()(string msg)
{
cout << "msg: " << msg << endl;
}
};
int main(void)
{
Test t;
t("我是要成为海贼王的男人!!!"); // 仿函数
return 0;
}
- 是一个可被转换为函数指针的类对象
#include <iostream>
#include <string>
#include <vector>
using namespace std;
using func_ptr = void(*)(int, string);
struct Test
{
static void print(int a, string b)
{
cout << "name: " << b << ", age: " << a << endl;
}
// 将类对象转换为函数指针
operator func_ptr()
{
return print;
}
};
int main(void)
{
Test t;
// 对象转换为函数指针, 并调用
t(19, "Monkey D. Luffy");
return 0;
}
- 是一个类成员函数指针或者类成员指针
#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 统一了可调用对象的各种操作。
2. 可调用对象包装器
在 C++ 中,存在多种可调用对象,例如普通函数、函数指针、成员函数指针、仿函数(重载了()运算符的类对象)以及 C++11 引入的 lambda 表达式等。不同类型的可调用对象在使用时可能存在接口不统一的问题,std::function 就是为了解决这个问题而引入的。
std::function 是一个通用的多态函数包装器,它可以存储、复制和调用任何可调用对象。std::function 类型的对象可以绑定到普通函数、成员函数、函数对象和 lambda 表达式等,提供了一种统一的方式来处理不同类型的可调用对象。
2.1 语法与使用示例
基本语法:
#include <functional>
std::function<返回值类型(参数类型列表)> diy_name = 可调用对象;
std::function 的模板参数指定了可调用对象的签名,即返回类型和参数列表。以下是一些使用示例:
#include <iostream>
#include <functional>
// 普通函数
int add(int a, int b) {
return a + b;
}
// 仿函数
struct Multiplier {
int operator()(int a, int b) {
return a * b;
}
};
int main() {
// 定义一个 std::function 对象,用于存储返回值为 int,参数为两个 int 的可调用对象
std::function<int(int, int)> func;
// 绑定普通函数
func = add;
std::cout << "Add result: " << func(3, 4) << std::endl;
// 绑定仿函数
Multiplier multiplier;
func = multiplier;
std::cout << "Multiply result: " << func(3, 4) << std::endl;
// 绑定 lambda 表达式
func = [](int a, int b) { return a - b; };
std::cout << "Subtract result: " << func(3, 4) << std::endl;
return 0;
}
在上述代码中,std::function<int(int, int)> 定义了一个可以存储返回值为 int,接受两个 int 类型参数的可调用对象的包装器 func。然后分别将普通函数 add、仿函数 multiplier 和 lambda 表达式绑定到 func 上,并进行调用。
2.2 应用场景
- 回调函数管理:在编写需要注册回调函数的代码时,std::function 可以统一管理不同类型的回调函数。因为回调函数本身就是通过函数指针实现的,使用对象包装器可以取代函数指针的作用。
- 事件处理:在图形界面编程或游戏开发中,std::function 可以用于存储和调用事件处理函数。
3. 绑定器(std::bind)
std::bind 是一个函数模板,它可以将一个可调用对象与其参数进行绑定,生成一个新的可调用对象。通过 std::bind,可以预先绑定部分参数,也可以调整参数的顺序,从而实现更灵活的函数调用。
3.1 语法与使用示例
std::bind 的基本语法如下:
auto newCallable = std::bind(callable, arg1, arg2, ...);
其中,callable 是要绑定的可调用对象,arg1, arg2, ... 是要绑定的参数。可以使用 std::placeholders::_1, std::placeholders::_2, ... 来表示占位符,用于在调用新的可调用对象时传入参数。
以下是一些使用示例:
#include <iostream>
#include <functional>
// 普通函数
int add(int a, int b) {
return a + b;
}
// 成员函数
class Calculator {
public:
int multiply(int a, int b) {
return a * b;
}
};
int main() {
// 绑定普通函数,并预先绑定一个参数
auto addFive = std::bind(add, 5, std::placeholders::_1);
std::cout << "Add five result: " << addFive(3) << std::endl;
// 绑定成员函数
Calculator calc;
auto multiplyByTwo = std::bind(&Calculator::multiply, &calc, 2, std::placeholders::_1);
std::cout << "Multiply by two result: " << multiplyByTwo(3) << std::endl;
return 0;
}
在上述代码中,std::bind(add, 5, std::placeholders::_1) 绑定了普通函数 add,并预先绑定了第一个参数为 5,std::placeholders::_1 表示在调用 addFive 时传入的第一个参数。std::bind(&Calculator::multiply, &calc, 2, std::placeholders::_1) 绑定了成员函数 Calculator::multiply,并预先绑定了第一个参数为 2。
对于占位符的理解,可以看以下例子:
#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);
bind(output, placeholders::_1, placeholders::_2)(10, 20);
bind(output, placeholders::_2, placeholders::_1)(10, 20);
return 0;
}
示例代码执行的结果:
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);
3.2 应用场景
- 参数绑定与调整:当需要固定某些参数,或者调整参数顺序时,可以使用 std::bind。
- 适配接口:当接口要求的参数和实际可调用对象的参数不匹配时,std::bind 可以进行适配。
4. 可调用对象包装器与绑定器的结合使用
std::bind绑定器返回的是一个仿函数类型,得到的返回值可以直接赋值给一个std::function,在使用的时候我们并不需要关心绑定器的返回值类型,使用auto进行自动类型推导就可以了。因此std::function 和 std::bind 可以结合使用,将绑定后的可调用对象存储在 std::function 中,进一步提高代码的灵活性。
#include <iostream>
#include <functional>
int add(int a, int b) {
return a + b;
}
int main() {
// 绑定普通函数,并存储在 std::function 中
std::function<int(int)> addFive = std::bind(add, 5, std::placeholders::_1);
std::cout << "Add five result: " << addFive(3) << std::endl;
return 0;
}
在这个例子中,std::bind 绑定了 add 函数并预先绑定一个参数,然后将绑定后的可调用对象存储在 std::function<int(int)> 中,方便后续调用。
可调用对象包装器std::function是不能实现对类成员函数指针或者类成员指针的包装的,但是通过绑定器std::bind的配合之后,就可以完美的解决这个问题了,再来看一个例子,然后再解释里边的细节:
#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;
}
示例代码输出的结果:
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进行类型的自动推导,这样使用起来会更容易一些。