一、function与bind
1、什么是闭包?
在以往的c++程序中,回调一般都是用函数指针的方式来做,因为函数无法携带状态,这个其实是非常不方便的。可以使用闭包(closure) 来解决函数指针无状态问题。简单来说,闭包就是一个带状态的可执行体。它可以捕获函数需要的变量(值捕获或者引用捕获),然后在合适的时间来执行函数。通俗一点来说,一个函数,加上一些变量的组合,就是闭包。最能表示闭包含义的结构就是仿函数了,也就是重载了operator()的类。
在c++中,闭包(closure)的具体实现就是std::bind和lambda表达式,而function就是用来储存生成的闭包。
2、fucntion
2.1、什么是包装器?
std::function是一个函数包装器,该函数包装器模板能包装任何类型的可调用实体,如普通函数,函数对象,lamda表达式等。包装器可拷贝,移动等,并且包装器类型仅仅依赖于调用特征,而不依赖于可调用元素自身的类型。std::function是C++11的新特性,包含在头文件中。
2.2、使用
通过function对象来包装可调用实体,如下:
void func(int a)
{
cout << a << endl;
}
struct Display {
void operator()(int x){
cout << x << endl;
}
};
int main()
{
// 创建function对象
function<void(int)> callback;
// 定义函数指针
void (*pfnFunc)(int) = func;
// 1、function对象实例包装函数指针
callback = pfnFunc;
callback(10);
// 2、function包装函数
callback = func;
callback(10);
// 3、function包装对象函数
callback = Display();
callback(10);
// 4、function包装lamda表达式
callback = [](int a) {cout << a << endl;};
callback(10);
return 0;
}
2.3、使用function解决模板低效性
通过下面的示例讲解通过function解决模板低效性,如下:
int func(int a) {
return a;
}
struct Display {
int operator()(int x){
return x;
}
};
template<typename Type, typename Func>
Type use_func(Type type, Func func) {
static int num = 0;
num ++;
cout << "num = " << num << " addr: " << &num << endl;
return func(type);
}
int main()
{
int a = 20;
use_func(a, func);
use_func(a, Display());
return 0;
}
输出结果
num = 1 addr: 0x47e254
num = 1 addr: 0x47e250
Process returned 0 (0x0) execution time : 0.076 s
Press any key to continue.
在两次use_func函数调用中,模板参数Type都被设置成int,模板参数Func都接收一个int值并返回一个int值,好像Func的类型都相同,因此只会实例化模板一次。但是,根据实际的输出结果来看,use_func模板实例化了两次。use_func模板函数有一个静态成员num,可以根据它的地址来确认模板实例化了几次,通过输出结果来看有两个不同的地址,说明模板实例化了两次。
为了了解其中的原因,思考下编译器如何判断模板参数Func的类型,首先,来看函数调用use_func(a, func),其中的func是一个函数的名称,该函数接收int参数并返回int值,因此、模板参数Func的类型是int(*)int。use_func(a, Display())函数调用,第二个参数是Display对象,模板参数Func的类型是Display,此时会用Display类型再次实例化use_func模板。
通过function解决前面遇到的模板低效问题
注意到前面程序中的函数指针、函数对象有一个相同的地方,它们都接收一个int参数返回一个int结果,可以说它们的调用特征标是相同的。调用特征标是由函数返回值类型及参数列表共同构成,因此,这两个模板实例的调用特征标都是int(int)
模板function是在头文件functional声明的,它从调用特征标的角度定义了一个对象,可用于包装调用相同特征标的函数指针、函数对象或lambda表达式。要减少上面程序实例化次数,可以使用function<int(int)>创建二个包装器,在对use_func的二次调用中,让Func的类型都相同(function<int(int)>),因此只实例化一次,如下:
int main()
{
int a = 20;
function<int(int)> func1 = func;
function<int(int)> func2 = Display();
use_func(a, func1);
use_func(a, func2);
return 0;
}
输出结果
num = 1 addr: 0x47f250
num = 2 addr: 0x47f250
Process returned 0 (0x0) execution time : 0.007 s
Press any key to continue.
从输出结果来看,修改成使用function<int(int)>后,模板只实例化了一次
3、bind
3.1、定义
bind 是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表。常用的使用场景如下:
- 使用bind实现参数顺序的调整和将指定参数设置成固定值
- function 与 bind 结合后,便成为了 C++ 中类成员函数作为回调函数的一种规范的实现方式
3.2、使用
场景一:使用bind指定参数设置成固定值
#include <iostream>
#include <functional>
using namespace std;
void foo(int a, int b, int c) {
std::cout << "a = " << a << ", b = " << b << ", c = " << c << std::endl;
}
int main() {
auto func = std::bind(foo, 1, 2, std::placeholders::_1);
func(3); // 调用 func,实际上调用 foo(1, 2, 3),输出结果:a = 1, b = 2, c = 3
return 0;
}
场景二:使用bind调整参数顺序
#include <iostream>
#include <functional>
using namespace std;
void display(int a, double b) {
std::cout << "a = " << a << ", b = " << b << std::endl;
}
int main() {
auto func = std::bind(display, std::placeholders::_2 ,std::placeholders::_1);
int a = 20;
double b = 30.5;
func(b, a); // 原始的display函数参数顺序是(int, double),使用bind后参数的顺序调整成(double, int),使用bind调整了参数顺序
return 0;
}
场景三:类成员函数作为回调函数
#include <iostream>
#include <functional>
using namespace std;
typedef std::function<void(int)> GeneralCB;
// 创建 function 对象
template<typename T>
static GeneralCB CreateCB(void (T::*func)(int), T* object)
{
if (object)
{
return std::bind(func, object, std::placeholders::_1);
}
return nullptr;
}
class Observer {
public:
void Execute() {
m_cb(20);
}
public:
GeneralCB m_cb;
};
class Subject {
public:
Subject()
{
m_pObserver = new Observer;
m_pObserver->m_cb = CreateCB(&Subject::Display, this);
}
void Display(int a) {
cout << "a = " << a << endl;
}
void Execute() {
m_pObserver->Execute();
}
private:
Observer *m_pObserver;
};
int main() {
Subject obj;
obj.Execute();
return 0;
}
3.3、std::placeholders
占位符,c++11中有29个占位符,分别是_1~_29,一般情况下是std::placeholders::_1这样的。占位符的作用就是用来代表参数的,std::placeholders::_1表示的是std::bind创建的std::function对象被调用时,传入的第一个参数,而std::placeholders::_2则是第二个参数依次类推。
调整std::placeholders::_x在std::bind时的顺序,就可以起到调整参数顺序的作用了。此外,也可以在std::bind的时候不用std::placeholders::_x,而直接写成固定的值,这样子调用std::function存储的对象时,对应位置的参数将是固定值。
#include <iostream>
#include <functional>
using namespace std;
void Display(int a, int b) {
cout << "a = " << a << ", " << "b = " << b << endl;
}
int main() {
auto f1 = std::bind(Display, std::placeholders::_1, std::placeholders::_2);
auto f2 = std::bind(Display, std::placeholders::_2, std::placeholders::_1);
auto f3 = std::bind(Display, std::placeholders::_1, 100);
f1(10, 20); // 输出结果:a = 10, b = 20
f2(10, 20); // 输出结果:a = 20, b = 10
f3(10, 20); // 输出结果:a = 10, b = 100
return 0;
}
3.4、std::bind 和 lambda表达式的一些区别
lambda底层的实现其实就是仿函数,而std::bind的实现,其实也是仿函数,和lambda不同的一个点是,lamdba的函数体是用户自己写的,而std::bind则是调用其他函数,所以std::bind就会比lambda多一个函数指针的大小。
#include <iostream>
#include <functional>
using namespace std;
void Func1(int32_t x)
{
std::cout << x;
}
void Func2(int32_t x, int64_t y)
{
std::cout << x << y;
}
int main()
{
int32_t x;
int64_t y;
auto f1 = std::bind(Func1, x);
auto f2 = std::bind(Func2, x, y);
auto ff1 = [x]() { std::cout << x; };
auto ff2 = [x, y]() { std::cout << x << y; };
std::cout << sizeof(f1) << " " << sizeof(f2) << std::endl; // 输出结果:8 24
std::cout << sizeof(ff1) << " " << sizeof(ff2) << std::endl; // 输出结果:4 16
return 0;
}