文章参考:
1. 可调用对象
一组执行任务的语句都可以视作一个函数、一个可调用对象。C++中提供了可调用对象
的概念,其应用相当广泛,例如在处理一些回调函数、触发函数时就会用到。
可调用对象有如下几种类型:
-
函数指针:
int add(int a, int b){ return a + b; } using func_ptr = int(*)(int, int); func_ptr p = add; cout << p(1, 2) << endl;
-
所属类通过
operator
重载了()
符号的对象(仿函数)class Test{ public: void operator()(string msg){ cout << msg << endl; } }; void main(void){ Test t; t("test"); // 使用重载操作符,输出了test return 0; }
-
可以被转换为函数指针的类对象:
#include <iostream> #include <string> using namespace std; using func_ptr = void(*)(string); // 函数指针 class Test{ public: static void hello(string msg){ cout << "hello," << msg << endl; } void show(string msg){ cout << msg << endl; } // 将对象转换为函数指针 operator func_ptr(){ // return show; 会报错:使用未声明但可以识别的标识符 return hello; // 必须使用静态函数 } }; int main(void){ Test t; t("world"); // 可以将对象当作函数指针来调用 return 0; }
注意:第18行的返回值必须是静态成员函数。否则会报错
use of undeclared identified "msg"
,即使用未声明的可识别标识符"msg"。 -
类成员函数指针或类成员指针:
#include <iostream> #include <string> using namespace std; class Test{ public: int num; static int age; public: static void hello(string msg){ cout << "hello," << msg << endl; } void show(string msg){ cout << msg << endl; } }; int Test::age = 10; int main(void){ // 静态成员函数 using func_ptr1 = void(*)(string); func_ptr1 p = Test::hello; p("zhangSan"); // 非静态成员函数 using func_ptr2 = void(Test::*)(string); func_ptr2 p2 = &Test::show; Test t; (t.*p2)("aaa"); // 因为运算符优先级,()的优先级高于* // 静态成员变量 int *p3 = &Test::age; *p3 = 18; // 非静态成员变量 int Test::* p4 = &Test::num; Test t2; (t2.*p4) = 10; return 0; }
满足以上任意一个条件,即可称为可调用对象,它们又被统称为可调用类型
。因为这些方法过于五花八门,较为复杂,所以C++11中提供了统一的可调用对象操作:std::function
、std::bind
。
2. 可调用对象包装器
std::function
是一个可调用对象的包装器,它是一个模板类,可以容纳除了类成员(函数)指针之外的所有可调度对象
。通过指定它的模板参数,它可以以统一的方式处理函数、函数指针、函数对象,并允许保存和延迟执行它们。
可调用对象包装器可以直接包装以下几种可调用对象:
- 普通函数。
- 类的静态成员函数。
- 仿函数。
- 对象转换成的函数指针。(
和仿函数在调用的形式上是一致
)
但是可调用对象包装器无法包装对象的成员变量、非静态成员函数
。
2.1 基本用法
头文件:
#include <functional>
std::function<返回值类型(参数类型列表)> 名字 = 可调用对象;
EG:
#include <iostream>
#include <string>
#include <functional> // 可调用对象包装器
using namespace std;
using func_ptr = void(*)(string); // 函数指针
class Test{
public:
// 静态成员函数
static void show_msg(string msg){
cout << msg << endl;
}
// 仿函数
void operator()(int id, string msg){
cout << id << " " << msg << endl;
}
// 将对象包装为函数指针
operator func_ptr(){
return show_msg;
}
};
// 普通对象
void show_msg(string msg){
cout << msg << endl;
}
int main(void){
// 包装一般函数
function<void(string)> p1 = show_msg;
// 包装静态成员函数
function<void(string)> p2 = Test::show_msg;
// 包装仿函数
Test t1;
function<void(int, string)> p3 = t1;
// 包装对象转换的函数指针(可以看出:对象转换的函数指针的包装和仿函数的包装是一样的
Test t2;
function<void(string)> p4 = t2;
// 调用
p1("aaa");
p2("bbb");
p3(1, "ccc");
p4("ddd");
return 0;
}
2.2 作为回调函数使用
回调函数本身通过函数指针实现,而对象包装器可以取代函数指针的作用
。
EG:
#include <iostream>
#include <string>
#include <functional> // 可调用对象包装器
using namespace std;
using func_ptr = void(*)(string); // 函数指针
class Base{
private:
// 将可执行对象包装类型作为成员变量
function<void(string)> call_back;
public:
// 用构造函数,将可执行对象传进来
Base(function<void(string)> var): call_back(var){}
// 执行传进来的可执行对象
void notify(string msg){
call_back(msg);
}
};
class Test{
public:
// 静态成员函数
static void show_msg(string msg){
cout << msg << endl;
}
// 仿函数(因为仿函数和对象转换的指针函数形式一致,为了避免重复,这里参数列表使用(int, string)
void operator()(int id, string msg){
cout << id << " " << msg << endl;
}
// 将对象包装为函数指针
operator func_ptr(){
return show_msg;
}
};
// 普通对象
void show_msg(string msg){
cout << msg << endl;
}
int main(void){
// 一般函数
Base b1(show_msg);
b1.notify("一般函数");
// 静态成员函数
Base b2(Test::show_msg);
b2.notify("静态成员函数");
// 仿函数(因为参数列表和Base构造函数的包装器的参数列表不一致,这里忽略)
// 包装对象转换的函数指针(可以看出:对象转换的函数指针的包装和仿函数的包装是一样的
Test t1;
Base b3(t1);
b3.notify("对象转换的函数指针");
return 0;
}
从这个例子可以看出:使用对象包装器可以非常方便的将可调用对象转换为一个函数指针,并由此实现函数的回调,程序的灵活性得到的大大的提升。
3. 绑定器
3.1 概述
作用:
std::bind
用来将可调用对象和其它参数绑定在一起,得到一个仿函数
,仿函数可以使用包装器std::function
进行保存,并延迟调用。简单地说,有以下两种作用:
- 将可调用对象与其参数绑定成一个仿函数。
- 将n元(参数个数为n个)可调用对象转换为m元(参数个数为m个,m<=n)可调用对象,也就是只绑定部分参数。
语法:
-
绑定除了类的非静态成员函数、类的成员变量以外的可调用对象:如类的静态成员函数、仿函数
auto f = std::bind(可调用对象的地址, 绑定的参数/占位符); f(实参);
-
绑定类的非静态成员函数/类的非静态成员变量(如果是成员变量,那么参数/占位符不用写):
auto f = std::bind(类函数/成员地址, 类实例对象地址, 绑定的参数/占位符); f(实参);
3.2 占位符
placeholders::_n
:代表占位符所在位置的参数将会被仿函数
调用时第n
个实参所替换。
Eg:
-
代码:
#include <iostream> using namespace std; void show_msg(int id, string name) { cout << id << "name" << endl; } int main(void){ auto f1 = bind(show_msg, placeholders::_1, "zhangSan"); f1(1); f1(2, "liSi"); return 0; }
-
输出:
1, zhangSan 1, zhangSan
-
分析:
第10行:绑定器绑定可调用对象
show_msg
。第一个参数使用占位符placeholders::_1
,意思是会将调用f1
时传递来的实参中第一个参数替换在占位符的位置。第二个参数是确认绑定的参数,即使在调用f1
时传入了第二个参数,依旧使用绑定时确定的参数。因此,通过
placeholders::_n
占位符,我们实现仅绑定部分参数,其余参数在调用时再确定。
3.3 绑定类非静态成员函数/变量
可调用对象包装器std::function
无法对类的非静态成员函数
或类的非静态成员变量
进行包装,但是通过绑定器std::bind
的配合,我们可以完美解决这个问题:
EG:
-
代码:
#include <iostream> #include <string> #include <functional> // 可调用对象包装器 using namespace std; class Test{ public: string _name; Test(){} Test(string name): _name(name){} int add(int a, int b); }; int Test::add(int a, int b){ cout << a << "+" << b << "=" << a + b; return a+b; } int main(void){ // 绑定非静态成员函数 Test t1; auto b1 = bind(&Test::add, &t1, placeholder::_1, 0); b1(1); function<int(int, int)> f1 = b1; f1(1, 2); function<int(int)> f2 = b1; f2(1); // 绑定非静态成员变量 Test t2("zhangSan"); auto b2 = bind(&Test::_name, &t2); b2() = "liSi"; cout << b2() << endl; function<string&(void)> f3 = b2(); cout << f3() << endl; f3() = "wangWu"; cout << f3() << endl; }
-
输出:
1+0=1 1+0=1 1+0=1 liSi liSi wangWu
-
分析:
- 第
21
、27
行:通过bind
可以将类的非静态成员函数/非静态成员变量绑定为仿函数,从而以类的形式调用。因此第21
、27
行可以执行。 - 第
23
、25
行:绑定器bind
绑定时可以通过占位符、默认参数来实现对可调用对象参数列表中参数数量的调整,因此第23
、25
行都是正确的。 - 第
32
行:对于由非静态成员 变量绑定得到的仿函数,在使用保证器进行包装时,返回值类型为该变量的类型,参数列表为空,因此使用void
。需要注意:如果要对变量进行修改而不是只读,那么返回的应该是一个引用。
- 第
注意:
到这里,借助包装器和绑定器,除了类的静态成员变量
,其余可调用对象 都可以进行统一的调用。`