C++11:仿函数&&lamda表达式&&bind&&函数指针

前言知识概况
对于直接哪一个变量名就能调用一个代码段,这是自从c开始大家一直在追求的,一开始大家封装函数调用,用函数名来调用,后来拓展了需求,用一个函数指针来传递想调用的函数,也就是所谓函数中调用函数成了回调函数,在后来函数指针的书写实在是头疼,大家就用typedef来封装函数指针类型,再到后来
面向对象了,大家就用仿函数咯,一夜解放咯,但是很多场景仿函数还是不够方便,就有了迅速构造一个可调用对象,也就是lamda本质上还是仿函数(或者叫做匿名对象函数)哦。
再后来为了解决可调用对象的类型也可能你是函数啊,也可能是仿函数也可能是lamda表达式,就导入了function包装器,他用来包装一个可调用对象,以及可调用对象的类型没有必要我们去写(尤其是lamda表达式)
再提及到后来遇见的bind,那是因为在某些时候使用的函数接口我们想修改参数,这也是有这样的需求(参数绑定器)
一. 调用对象
1.1 函数指针&&函数&&typedef封装
typedef void(*str_print_func)(const char*);//自定义类型命名
void func(const char* str) {
cout << str;
}
int main() {
str_print_func str_print = func;// 把函数给函数指针
const char* test_str = "hello wold";
str_print(test_str);
return 0;
}
简单提一下函数指针 如你的函数类型 void func(int a) 那么你函数类型也计算 void (int )
将来对于函数指针就是 void(*类型名)(参数类型) void(*myname)(int a )
1.1.1 函数指针方式的回调函数
起始最常见的就是qsotr啦,为了让你比较一个对象的排序他也可以通过传入一个函数指针的方式来调用,如下图对于qsort的传入的仿函数对象是一个函数指针:
// 上文提到了函数指针的理解 // 那么迅速上手咯,函数指针名compare 函数类型是 int (const void*,const void*) int (*compare)(const void*,const void*)// 参数两个都是这里值得理解的是这里两个类型都是void*的,怎么回事,还记得之前在C/C++ ---条件编译#ifdef&&如今系统编程中void*的还能用?&&NULL何去何从?-CSDN博客
这篇博客有提及关于void*的妙用也就是说它可以接受来自任意类型的参数,那么接下来我们开始使用它吧。


//qsort
struct ware {
int price_;
string name_;
int weight_;
};
int ware_compare(const void * w1, const void * w2)
{
return (*(const struct ware*)w1).price_ < (*(const struct ware*)w2).price_;
}
int main() {
// 定义一系列商品拿来我们的仿函数排序吧!
struct ware wares[] = {{12,"西瓜",10},{100,"手表",2},{55,"菠萝",10},{33,"窗台",2}};
for (int i = 0;i < 4;i++) {
cout << wares[i].name_ << " ";
}
// 建议提前typedef一些会更方便
// 要求传入一个函数指针
int(*mycompare)(const void * , const void * );
mycompare = ware_compare;// 函数给该指针吧
qsort(wares,4,sizeof(struct ware), mycompare);
// 打印前后看看
cout << endl;
for (int i = 0;i < 4;i++) {
cout << wares[i].name_ << " ";
}
}
1.2 仿函数
仿函数,仿函数其实就是一个类公用封装的赋值重载函数 ,它的作用就是像一个函数一样使用如下代码
class myfunctor {
public:
// 重载 operator()
void operator()(const string& str) {
// 这个函数的作用就是拿来打印 这就是我的仿函数
cout << str;
}
};
void test(myfunctor func_use,const string str) {
// 还可以像回调函数一样直接传参进来使用
func_use(str);
}
void test_functor() {
// 创建一个仿函数对象就可以把该对象当中函数一样调用
// 故该对象就像一个函数一样
myfunctor str_functor;
str_functor("hello world");// 像一个函数一样调用
string str = "test for my functor";
test(str_functor,str);// 直接充当回调函数
}
int main() {
test_functor();
return 0;
}
总之有了仿函数对象之后,我们就不用像函数指针一样类型可读性挺差的,甚至用起来也挺复杂的,所以有了仿函数指针之后,美滋滋! 我们对类型的把控就是,直接把一个类的实例化对象当函数调用,而且类名由我们自定义,这个的体现可以在现在c++的sort可以体现如下:
1.2.1 sort需要的仿函数对象 
// 定义一个类 叫做商品类 将来希望用price进行排序
class ware {
public:
int price_;
string name_;
int weight_;
};
// 定义比较使用的仿函数对象
class ware_compare {
public:
bool operator()(const ware& w1, const ware& w2) {
return w1.price_ < w2.price_;// 返回一个比较价格的函数
}
};
void test_sort_ware() {
// 定义一系列商品拿来我们的仿函数排序吧!
vector<ware>wares = { {12,"西瓜",10},{100,"手表",2},{55,"菠萝",10},{33,"窗台",2} };
for (auto ch : wares) {
cout << ch.name_<<" ";
}
std::sort(wares.begin(),wares.end(),ware_compare());
// sort对于这样不能直接比较大小的需要你传入一个仿函数对象 我们可以传递一个匿名对象进去
// 打印前后看看
cout << endl;
for (auto ch: wares) {
cout << ch.name_<<" ";
}
}
int main() {
//test_functor();
test_sort_ware();
return 0;
}

1.3 lamda表达式
lamda表达式 ,C++11以后最不像c++的一个语法之一,但是不得不说它用起来很舒服,虽然一开始用起来很头疼,但是在一些小小的需要她的场景里面她真的很受用,虽然也有她的缺点,下面来介绍她吧
1.3.1 lamda表达式的语法
一个完整的lamda表达式长这样 ;
[捕获列表](参数 可以不带变量名)->返回值{ // 函数体 }本质上他依旧是一个仿函数对象,只不过类型由编译器自己来决定,但是好处就是我们可以直接用他啦。
void test_lamda() {
int a = 10;
string b = "hello";
// use _syntax
auto func_lamda = [a, b](string str, int v)->int {
// 打印捕获的对象,以及参数
cout << a << endl;
cout << b << endl;
cout << str << endl;
cout << v << endl;
return 0;// 定义了返回值 必要返回哈
}; // 注意这里也要加分号因为他依然是一个仿函数对象这里相当于你定义了一个对象
//调用这个可调用对象吧
func_lamda("hello",2);
}
int main() {
test_lamda();
}
1.3.2 lamda的细节
首先你要深刻理解在c++中lamda依旧是一个仿函数的对象,他的类型将来由编译器指定,所以你进来用auto 来让编译器指定,再其次就是他哪些可以省略,以及哪些一定不可以省略,以及常性。
捕获类型 和省略的参数:
void test_lamda_detail() {
/*********
*
* 1.lamda表达式 省略
* 2.捕获列表和函数体一定不能省略
* 3. 捕获的常性
*********************/
//如下是最简单的lamda对象
auto func1 = [] {};
// 捕获的类型 有
/***********************
*
* 1. 捕获列表获取的方式有:
* [var_name]:值拷贝传递 [&ver_name] : 这是引用传递哦 不说取地址
* [=] : 这表示按照值拷贝捕获上文全部的对象,变量甚至this指针比如你在一个类的成员函数(非静态的)
* [&] : 这表示按照引用的方式捕获上文的全部对象
* eg. [&this] : 这表示按照引用方式捕获当前的this指针
*
****************************/
int a = 10;
string b = "hello world";
auto func2 = [=]{
cout << a << endl;
cout << b << endl;
};// 返回值通常可以不写编译器可以自动推导 你在里面返回值的类型
//func2();
}
int main() {
test_lamda_detail();
return 0;
}
func2调用

传值捕获的常性以及mutabel
如下图1,2 一个是 按照传值拷贝的方式捕获 一个是按照传引用的方式捕获,但是离谱的传值拷贝的方式居然不能修改,但是传引用捕获天然就可以修改,真是离谱: 简单介绍一下吧
设计者的思路大概是这样的,虽然你是传值拷贝,但是我决定你只是想拿来打印或者看一下,并不想使用,所以将来编译器将lamda表达式封装成仿函数对象的时候大概如下:
图1 ,下图2

大概原理:
// 传值实现的仿函数
class lamda_1_compile_1{
public:
void operator(const string str,const int a){
// 填充你写的函数体
}
}
// 传引用
class lamda_2_compile_1{
public:
void operator( string& str,int & a){
// 填充你写的函数体
}
}
mutable打破常性但是依旧拷贝对象
如下图也就证明了当使用了mutable就可以打破传值传参的常性,对了一个细节,当你使用传值捕获然后使用mutable的时候参数列表一定要加上哪怕你没有加上,这就是语法,不用问为什么哈哈哈 。
在函数体内可以修改:

出了函数体又回到解放前:

测试代码:
void test_const_lamda() {
string str = "hello";
int a = 10;
int b = 30;
auto func3 = [=]()mutable {
// 哪怕我用值拷贝捕获都不能修改
a++;
b++;
str = "can i modify?";
};
func3();
}
int main() {
test_const_lamda();
return 0;
}
1.3.4 可是我不想修改传引用的对象!
很可惜哈我发现这是c++17才支持的标准哈,如下图,


1.3.5 lamda的底层是啥?
就是一个例子 lamda本质上还是一个仿函数对象但是是由编译器自动生成自动命名,以及自动帮你填充函数体 : 如下实验:
如图仿函数的类型名就是 class myoperator 但是对于lamda表达式名字是:
class `void __cdecl test_lamda(void)'::`2'::<lambda_1>
本身还是一个类嘛 class 实际上也是一个仿函数,这个名字有点长这跟编译器有关名字是他指定的这我们就不用过多关系了
//对比用的仿函数对象
class myoperator
{
public:
void operator()(int a, int y) {
// 啥也不做
}
};
void test_lamda() {
auto myfunc_lamda1 = [](int a,int b) {
///啥也不做的函数体
};
auto myfunc_lamda2 = []( int b) {
///啥也不做的函数体
};
myoperator p1;
// 打印类型
cout << typeid(myfunc_lamda1).name() << endl;
cout << typeid(myfunc_lamda2).name() << endl;
cout << typeid(p1).name() << endl;
}

1.3.6使用常见 ---sort
既然说到这了,我们的lamda使用场景就是当我们使用简单的需要调用的仿函数对象的时候很方便如下sort : 这样可读性也很强
#include<algorithm>
void test_lamda_sort(){
vector<int> ret{1,2,3,4,5,6};
std::sort(ret.begin(),ret.end(),[](int a,int b){
return a<b; // 返回类型将来会自动推导这里就省略了
});
}
二. function 包装器
2.1 可调用对象的类型
到现在对于可调用对象,我们学了三种: 1. 函数指针,2. 仿函数3. lamda表达式 :
举例函数指针类型 我们通常可以用: typefef void(*func_type) (int ,int) 也就能用就是没什么可读性
2. 对于仿函数的使用,仿函数挺好用的也很舒服,命名也是自定义的,也很方便,但是弊端就是要我们自己手写一个类,还要自己命名,对于一些简单的类还不如用lamda呢 。
但是问题是 lamda的类名除了用auto我别无选择,所以为了统一类型的使用,funciton是一个包装了你的可调用对象:
#include<functional>
int test_name(int id, string name) {
cout << name;
return id + 2;
}
class myfunctor {
public:
int operator()(int id, string name) {
/// 示例的仿函数
return 0;
}
};
void test_function() {
// 包装器可以接受指定类型的可调用函数
function<int(int, string)> my = test_name;
myfunctor f2;
my = f2;// 可以传递仿函数对象
my = [](int, string) {
// 这是lamda表达式
cout << "hello im lamda" << endl;
return 1;
};
// 至始至终 都是同一个function对象在接受其他的可调用对象
// 本质上就是function内部封装了你传递进来的可调用对象
// 当你掉用function的时候他就会调用他内部仿函数封装的其他的调用对象
// 最终调用你想调用的对象
my(1,"hello");
}
int main() {
test_function();
return 0;
}

2.2 实例 function 统一可调用对象
总之之后funciton<> 也可以作为一个类别使用,所有其他的可调用对象都可以封装到我们的function中这样就非常方便了,如下 举例一个求后缀和的 逆波兰表达式的优化
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
string sign_ = "+-*/";
for (auto& str : tokens)
{
if (str.find(sign_)!=string::npos)
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
switch (str[0])
{
case '+':
st.push(left + right);
break;
case '-':
st.push(left - right);
break;
case '*':
st.push(left * right);
break;
case '/':
st.push(left / right);
break;
}
}
else
{
// 1、atoi itoa
// 2、sprintf scanf
// 3、stoi to_string C++11
st.push(stoi(str));
}
}
}
};
优化的思路 : 我们把switch的对应的方法放入一个kv的映射里面去 每一个k: ”+ -*/ “都对应一种计算的方法 : 本质上思路就是,模拟switch ,以及理解对于可调用对象。
#include<unordered_map>
class Solution {
public:
int evalRPN(vector<string>& tokens) {
unordered_map<string, function<int(int, int)>> hash_map = {
{ "+", [](int l, int r) { return l + r; } },
{ "-", [](int l, int r) { return l - r; }},
{ "*", [](int l, int r) { return l * r; }},
{ "/", [](int l, int r) { return l / r; }},
};
stack<int> st;
string sign_ = "+-*/";
for (auto& str : tokens)
{
if (sign_.find(str)!=string::npos)
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
int ret= hash_map[str](left,right);
st.push(ret);
}
else
{
// 1、atoi itoa
// 2、sprintf scanf
// 3、stoi to_string C++11
st.push(stoi(str));
}
}
}
};
三 . std::bind
3.1 bind的作用
好消息好消息,bind,function,lamda 这些家伙真的我哭死,他们都是一家人底层都是在不断地复用仿函数,因为舒服,bind的作用是封装可调用对象,对于一些参数的接口进行封装固定参数形成新的可调用给下给你用。
function用来统一可调用对象,lamda用来迅速创建可调用对象。

如下代码,他的使用很舒服,就是bind(可调用对象,然后是固定参数 用placeholders:: _ )
值得注意的地方是,这跟_1 代表将来你传参是第几个数然后传到这哦,这表示的是占位符。
void test_bind() {
// 封装lamda
auto add_name = [](std::string name, int a, int b) { std::cout << name<<endl; return a + b; };
// 封装固定参数名字 就是add
std::function<int(int, int)> add = std::bind(add_name,"add", std::placeholders::_1, std::placeholders::_2);
int ret = add(3,4);
std::cout << ret;
}
int main() {
test_bind();
return 0;
}

占位符的体现
void test_bind() {
// 封装lamda
auto sub_name = [](std::string name, int a, int b) { std::cout << name<<endl; return a - b; };
// 封装固定参数名字 就是add
std::function<int(int, int)> sub = std::bind(sub_name,"sub", std::placeholders::_2, std::placeholders::_1);
int ret = sub(9999,2);
std::cout << ret;
}
int main() {
test_bind();
return 0;
}

3.2 绑定成员函数
你也没有试过,把一个对象的公有的成员函数取出来过,成员函数虽然有作用域但是,如果他是公用的我们还是可以拿出来用的,但是他有一个细节就是,他的第一个参数是当前对象指针this ,这跟你要记住那我们尝试把一个成员函数拿出来然后传入一个对象然后绑定成一个用起来跟普通函数一样的家伙吧
class myobject {
public:
int sayname_with_add(string name ,int x,int y) {
std::cout << name << std::endl;
return x + y;
}
};
int main() {
myobject ob;
// bind第一个参数是可调用对象 然后我传入了第一个i固定的参数就是一个对象指针,然后是固定字符串
// 然后两个占位符
function<int(int, int)>say_hi_add = bind(&myobject::sayname_with_add,&ob,"hi", std::placeholders::_1, placeholders::_2);
cout << say_hi_add(3,100) << endl;
return 0;
}

3.3 bind能修改返回值吗?
先明确拒绝你,不能,哈哈哈他只能强转返回值哈,但是强转是要注意的哈

如下代码:
int test_func(string name,int a,int b) {
return a + b;
}
void test() {
function<double(int, int)>f = std::bind<double>(test_func,"hello",1,std::placeholders::_1);
// 我们把返回值 强转为double注意我只是强转不是我想改就改 要注意强转是否
}

更多推荐


所有评论(0)