C++中引入bind绑定器和function函数对象
一步一步来,先学习下bind1st和bind2st的底层原理,才好理解后面的bind绑定器和function函数对象的底层原理。
bind绑定器
bind1st和bind2nd什么时候会用到
介绍:bind1st是一个辅助模板函数,它创建一个适配器,通过将二元函数的第一个参数绑定为指定值,将二元函数对象转换为一元函数对象。
bind2nd类似,很好理解。
先举个实例来说明绑定器具体有什么应用场景:
#include <iostream>
#include <vector>
#include <ctime>
#include <functional>
#include <algorithm>
using namespace std;
template<typename Container>
void showContainer(Container& con){
for(auto e: con)
cout<< e << " ";
cout << endl;
}
int main(){
srand(time(nullptr));
vector<int> vec;
for(int i = 0; i < 10; ++i)
vec.push_back(rand() % 100 + 1);
// sort(vec.begin(), vec.end()); // 默认升序
// showContainer(vec);
sort(vec.begin(), vec.end(), greater<int>()); // 降序
showContainer(vec);
/*
这个时候我想把77 插入到降序的vec中,也就是用find_if, 它的第三个参数需要一个一元函数对象。
auto iter = find_if(vec.begin(), vec.end(), ###);
如果我们想用greater<int> 来实现,是不行的,因为greater<int>是一个二元函数对象,它接受两个参数。
怎么办呢?这时就可以用到绑定器,把77绑定到greater<int>的第一个参数,而后返回一个一元函数对象,作为find_if的第三个参数。
当遇到第一个77 > elem 的元素时,返回elem的迭代器,然后把77插入该位置即可。
*/
auto iter = find_if(vec.begin(), vec.end(), bind1st(greater<int>(), 77));
vec.insert(iter, 77);
showContainer(vec);
}
bind1st和bind2nd底层原理
以下实现了my_find_if和my_bind1st函数。
#include <iostream>
#include <vector>
#include <ctime>
#include <functional>
#include <algorithm>
using namespace std;
template<typename Container>
void showContainer(Container& con);
// 手动实现my_find_if有助于理解绑定器
template<typename Iterator, typename Compare>
Iterator my_find_if(Iterator first, Iterator last, Compare comp){
for(; first < last; ++first){
// 可见 find_if 需要一个一元函数对象
if(comp(*first)) return first;
}
}
// 手动实现一个my_bind1st
// 首先要看懂这是个啥? 这是个一元函数对象类类型
template<typename Compare, typename T>
class _mybind1st{
public:
_mybind1st(const Compare& comp, T val): _comp(comp), _val(val){}
bool operator()(T& second){
return _comp(_val, second);
}
private:
Compare _comp;
T _val;
};
template<typename Compare, typename T>
_mybind1st<Compare, T> my_bind1st(const Compare& comp, const T& val){
return _mybind1st<Compare, T>(comp, val);
}
int main(){
srand(time(nullptr));
vector<int> vec;
for(int i = 0; i < 10; ++i)
vec.push_back(rand() % 100 + 1);
// sort(vec.begin(), vec.end()); // 默认升序
// showContainer(vec);
sort(vec.begin(), vec.end(), greater<int>()); // 降序
showContainer(vec);
auto iter = my_find_if(vec.begin(), vec.end(), my_bind1st(greater<int>(), 77));
vec.insert(iter, 77);
showContainer(vec);
}
template<typename Container>
void showContainer(Container& con){
for(auto e: con)
cout<< e << " ";
cout << endl;
}
首先介绍一下这个类:_mybind1st
_mybind1st是个一元函数对象类类型, 这一点可以从重载的函数调用运算符可以看出,它只接受一个参数second。
其次它有两个成员变量,一个是Compare类型的_comp,一个是准备绑定到bind1st的第一个位置上的参数。
因为我们在类里存储了二元函数对象,那么在重载的函数调用运算符中就可以调用这个它,并且可以先将事先传过来的参数_val“作为默认形参”绑定到_comp的第一个参数上,然后调用运算符接受一个参数作为这个一元函数对象的唯一参数。至此_mybind1st就是一个一元函数对象类型了。
为什么非要定义一个返回一元函数对象的函数my_bind1st呢?
这是因为在C++17以前,模板类是不能参数推导的,为了省去我们自己给定类模板参数的麻烦,我们先让函数把comp和val的类型推导出来,然后传给_mybind1st作为类模板参数。
举个自己传递类模板参数的例子:
// 把这一行
auto iter = my_find_if(vec.begin(), vec.end(), my_bind1st(greater<int>(), 77));
// 改成
auto iter = my_find_if(vec.begin(), vec.end(), _mybind1st<greater<int>, int>(greater<int>(), 77));
// 也是可以运行的
function函数对象
function函数对象什么时候会用到
首先说一下为什么会用到function函数对象。
因为不论是绑定器,函数对象,lambda表达式,它们只能用在一条语句中,如果我们想把它们的类型留下来,那就要用到function函数对象。
function函数对象包装函数
先举个简单例子:
void hello1(){
cout<<"hello world!"<<endl;
}
void hello2(string str){
cout<<str<<endl;
}
int main(){
// void(string) 是一个函数类型,不是函数指针类型
function<void(string)> func2(hello2);
func2("hello world!"); // func1.operator(string) => hello2(string)
}
这个简单的例子使用函数指针也可以实现,接下来举个例子说明function函数对象的必要性:
int main(){
// 如果我这么写
void(*pfunc1)() = hello1; // 正确
// 如果尝试使用函数指针绑定到函数对象上:
// error: 不存在从 "std::_Bind<void (*(const char *))(std::string str)>" 到 "void (*)()" 的适当转换函数C/C++(413)
void(*pfunc2)() = bind(hello2, "hello world!");
pfunc1();
pfunc2(); // 报错
}
可以发现函数指针只能指向不同函数,不能指向函数对象。
function函数对象包装lambda表达式
int main(){
function<int(int, int)> func3([](int a, int b)->int{ return a + b; });
cout << func3(1, 2) << endl; // 3
}
function函数对象包装成员函数
class Test{
public:
void hello(string str){ // 通过函数指针调用:void (Test::*pfunc)(string)
cout << str << endl;
}
};
int main(){
function<void(Test*, string)> func4(&Test::hello);
Test t;
func4(&t, "call Test::hello");
}
- 用函数类型实例化function;
- 通过function调用operator的时候,需要根据函数类型传入相应的参数。
举个实际点的使用场景
假设要做一个图书管理系统:
int main(){
int choice = 0;
while(1){
cout << "-----------------" << endl;
cout<< "1.查看所有书籍信息" << endl;
cout<< "2.借书" << endl;
cout<< "3.还书" << endl;
cout<< "4.查询书籍" << endl;
cout<< "5.注销" << endl;
cout<< "请选择";
cin >> choice;
switch(choice){
case 1:
break;
case 2:
break;
case 3:
break;
case 4:
break;
case 5:
break;
default:
break;
}
}
}
这样写好不好呢?当然不好,因为当我们增加、删除某个功能的时候,这里的业务代码整个就要重新修改、编译,不符合设计模式中的开闭原则,难以维护。
这个时候就要引出function函数对象了。
我们这样来设计这个功能,首先来新建一个action.h,里面包含一个Action类和menu函数:
// action.cpp
#include <iostream>
#include <functional>
#include <string>
#include <unordered_map>
using namespace std;
class Action{
public:
Action(){
actionMap.insert({1, doShowAllBooks});
actionMap.insert({2, doBorrow});
actionMap.insert({3, doBack});
actionMap.insert({4, doQueryBooks});
actionMap.insert({5, doLoginOut});
}
function<void()> getSolution(int choice){
auto it = actionMap.find(choice);
if(it == actionMap.end()){
return Error;
}
else return actionMap[choice];
}
static void doShowAllBooks(){ cout << "查看所有书籍信息" << endl; }
static void doBorrow(){ cout << "借书" << endl; }
static void doBack(){ cout << "还书" << endl; }
static void doQueryBooks(){ cout << "查询书籍" << endl; }
static void doLoginOut(){ cout << "注销" << endl; }
static void Error(){ cout << "输入的选项无效,请重新输入!" << endl; }
private:
unordered_map<int, function<void()>> actionMap;
};
inline void menu(){
cout << "-----------------" << endl;
cout<< "1.查看所有书籍信息" << endl;
cout<< "2.借书" << endl;
cout<< "3.还书" << endl;
cout<< "4.查询书籍" << endl;
cout<< "5.注销" << endl;
cout<< "请选择:";
}
我们的主业务源文件就变成:
// main.cpp
#include <iostream>
#include <functional>
#include <string>
#include <unordered_map>
#include "action.h"
using namespace std;
int main(){
Action action;
int choice = 0;
while(1){
menu();
cin >> choice;
auto solution = action.getSolution(choice);
solution();
}
}
经过这样的设计,当我们有共模块的增删改需求时,我们只需要修改action类和菜单函数,而不需要去动主业务模块,它已经完全闭合。
有人说不用function函数对象,用函数指针也是可以实现这样的设计的,是的确实。但是如果功能模块使用的是bind绑定器,或者lambda函数对象,函数指针就无能为力了。
funtion的实现原理
一个简单的function函数对象实现:
#include <iostream>
#include <functional>
#include <string>
#include <unordered_map>
using namespace std;
void hello(string str){
cout<<str<<endl;
}
template<typename Fty>
class myfunction{};
template<typename R, typename A1>
class myfunction<R(A1)>{
public:
using PFUNC = R(*)(A1);
myfunction(PFUNC pfunc): _pfunc(pfunc){}
R operator()(A1 arg){
return _pfunc(arg);
}
private:
PFUNC _pfunc;
};
int main(){
myfunction<void(string)> func(hello);
func("hello");
}
使用可变模板参数实现接收任意参数。
```c++
template<typename R, typename... A>
class myfunction<R(A...)>{
public:
using PFUNC = R(*)(A...);
myfunction(PFUNC pfunc): _pfunc(pfunc){}
R operator()(A... arg){
return _pfunc(arg...);
}
private:
PFUNC _pfunc;
};
可以看到其内部就是用函数指针来保存包装的函数,但上面说了,函数指针是不能指向bind绑定器等一类的函数对象的,所以怎么办呢? 这里我也不知道,可能要去看function的源码才能知道如何实现的,function源码详解,挺深的以后再看。
lambda表达式
首先说一下lambda表达式的优势。
首先说一下函数对象的缺点,函数对象一般用在泛型算法中的参数传递,包括比较性质的/自定义操作。
那么为了实现这些操作,就要定义一个仿函数的类型,但也许用完之后,就再也用不到这个类型了,显然易见定义一个这样的类型是没有必要的。
因此lambda表达式就应运而生了,我们不需要新定义一个类型,直接使用lambda表达式就可以实现上述的需求。
lambda表达式的实现原理
先看一下lambda表达式的语法:
[捕获外部变量](形参列表)->返回类型{函数体}
#include <iostream>
using namespace std;
template <typename T = void> // 模板类型参数对应lambda的返回类型
class TestLambda01{
public:
TestLambda01(){} // 构造函数 对应lambda表达式的捕获列表
T operator()() const { // 函数调用运算符的形参列表对应lambda表达式的形参列表
cout << "hello world!" << endl; // 函数体对应lambda表达式的函数体
}
};
template <typename T = int>
class TestLambda02{
public:
TestLambda02(){}
T operator()(int a, int b) const {
return a + b;
}
};
int main(){
auto func1 = []()->void { cout << "hello world!" << endl;};
func1(); // hello world!
TestLambda01<>()(); // hello world!
auto func2 = [](int a, int b)->int { return a + b;};
cout << func2(1, 2)<< endl; // 3
cout << TestLambda02<>()(1, 2) << endl; // 3
}
接下来看下捕获列表的使用方法:
- []:表示不捕获任何外部变量;
- [=]:以传值的方式捕获外部的所有变量
- [&]:以捕获的方式捕获外部的所有变量
- [this]:捕获外部的this指针(this指针只能值捕获不能引用捕获)
- [=, &a]:以传值的方式捕获除a外所有的变量,以引用方式捕获变量a
- [a, b]:以传值的方式捕获外部变量a和b
- [&a, &b]:以引用的方式捕获外部变量a和b
int main(){
// lambda表达式只会捕获在定义它之前出现的变量
auto func3 = [&]()->void { cout << a << " " << b << " " << endl;}; // error: ‘a’ was not declared in this scope
int a = 1, b = 2;
// auto func4 = []()->void { cout << a << " " << b << " " << endl;}; // error: ‘a’ is not captured 、‘b’ is not captured
auto func4 = [&]()->void { cout << a << " " << b << " " << endl;};
func4(); // 1 2
{
func4(); // 1 2
auto func5 = [&a, &b]()->void { cout << a << " " << b << " " << endl;}; // 1 2
func5(); // 1 2
}
}
值传递不能够修改成员变量,因为函数调用运算符是个const方法:
#include <iostream>
using namespace std;
template <typename T = void>
class TestLambda03{
public:
TestLambda03(int a, int b):ma(a), mb(b){}
T operator()() const {
swap(ma, mb); // mismatched types ‘std::__cxx11::basic_string<_CharT, _Traits, _Alloc>’ and ‘const int’
}
private:
int ma;
int mb;
};
int main(){
int a = 1, b = 2;
// auto func6 = [=]()->void { swap(a, b);}; // mismatched types ‘std::__cxx11::basic_string<_CharT, _Traits, _Alloc>’ and ‘const int’
TestLambda03<>(a, b)();
}
如果想要修改:
#include <iostream>
using namespace std;
template <typename T = void>
class TestLambda03{
public:
TestLambda03(int a, int b):ma(a), mb(b){}
T operator()() const {
swap(ma, mb);
}
private:
mutable int ma;
mutable int mb;
};
int main(){
int a = 1, b = 2;
auto func6 = [=]()mutable ->void { swap(a, b);};
cout << a << " " << b << endl; // 值传递,没有改变
TestLambda03<>(a, b)();
}
如果是引用捕获,则不需要加mutable
因为在常函数中使用引用的话,改变的不是引用本身,而是改变引用的变量。
为什么只能值捕获this指针不能引用捕获this指针
首先,编译器是拒绝引用捕获this指针的,这里我举个简单的例子来说明引用捕获this指针会有哪些风险:
这段代码就类比于引用捕获,ra引用了This。本质上相当与ra是一个指向This 的指针常量,那么当This指向的内存被释放后,ra还是可以使用的,因为它还是保存了一个地址,只是内存被释放了,所以输出一个随机值。但是当This被置空后,ra就变成了悬空引用,它指向的内存被释放了,并且还被置了空。解引用就会产生未定义的行为。
#include<iostream>
#include<functional>
int main() {
int* This = new int(5);
int*& ra = This;
delete This;
std::cout << *ra << std::endl; // 1431655786
This = nullptr;
std::cout << *ra << std::endl; // 未定义
return 0;
}
而采用值捕获的话,pa是一个指针变量,它保存了了This保存的地址,This指向的内存被释放了,那么pa指向的内存自然也就被释放了;但是This被置了空,却不影响pa,因为它们是两个不同的对象。尽管不会产生未定义的行为,但是仍可能访问到无效的内存。
因此,必须使用值捕获的原因是,避免引用捕获this指针造成的悬空引用,进而产生的未定义行为。
#include<iostream>
#include<functional>
int main() {
int* This = new int(5);
int* pa = This;
delete This;
std::cout << *pa << std::endl; // 1431655786
This = nullptr;
std::cout << *pa << std::endl; // 1431655786
return 0;
}
lambda表达式的应用实践
既然lambda表达式只能使用在语句当中,如果想跨语句使用之前定义好的lambda表达式,怎么办?用什么类型来表示lambda表示式?当然使用function类型来表示函数对象的类型了。
假设要实现一个计算器:
#include <iostream>
#include <unordered_map>
#include <functional>
using namespace std;
int main(){
// 假设我要用一个map来存储消息类型和对应的回调函数
// 我现在知道要用lambda表达式,但我现在还不知道lambda表达式的具体实现,要用到lambda表达式的类型
unordered_map<int, function<int(int, int)>> calculateMap;
calculateMap[1] = [](int a, int b)->int {return a + b;};
calculateMap[2] = [](int a, int b)->int {return a - b;};
calculateMap[3] = [](int a, int b)->int {return a * b;};
calculateMap[4] = [](int a, int b)->int {return a / b;};
cout << "10 + 20 = " << calculateMap[1](10, 20) << endl;
}
智能指针删除器:
#include <iostream>
#include <unordered_map>
#include <functional>
#include <memory>
using namespace std;
int main(){
// 对于智能指针自定义删除器,如果打开的是一个文件,那么删除器显然要调用fclose
// 此时新定义一个类型是完全没必要的,完全可以使用lambda表达式
unique_ptr<FILE, function<void(FILE*)>> ptr1(fopen("data.txt", "w"), [](FILE* fp){ fclose(fp); });
}