文章目录
很高兴和大家见面,给生活加点impetus!!开启今天的编程之路!!
今天我们进一步c++11中常见的新增表达
作者:٩( 'ω' )و260
我的专栏:Linux,C++进阶,C++初阶,数据结构初阶,题海探骊,c语言
欢迎点赞,关注!!
C++进阶--C++11(04)
在c语言中,我们的可调用对象其实就只有函数
但是在c语言中,指针又会让我们难以掌握。
在之前的c++学习当中,我们学习到了仿函数,这也是一种可调用对象,但是仿函数要求一个类中必须实现operator()并且是公有,倘若我们只用实现一个最普通的加法函数,这样写会不会太杀鸡用牛刀了。显得有点小题大做了。接下来我们将学习lambda表达式,来解决这样的一个问题。
C++11之后,可调用对象就可以分为:函数指针,仿函数,lambda
lambda
lambda表达式语法
1::lambda表达式本质上是一个匿名函数对象,最好定义在函数体内部,因为没有类型,所以最好和auto一起使用。
为什么我们最好定义在内部?因为如果定义在全局,我不如直接写函数或者是仿函数。lambda表达式与其他两个可调用对象最大的区别就是它能够定义在函数内部,并且能够单独开一个自己的作用域。
为什么需要和auto配合使用:因为lambda表达式是一个匿名函数对象,如果我们想要使用这个可调用对象的话,肯定是需要将lambda表达式赋值给一个具体的对象,然后再对这个对象进行操作。
2:lambda表达式格式:
capture - list\] (parameters) -\> return type {function boby }
3:[capture-list]:叫做捕捉列表,写在lambda表达式的最前面,编译器根据[ ]来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉,下面会讲的更具体一点。即使捕捉列表为空,[ ]也不能省略,因为编译器根据这个来判断是不是lambda表达式的。
4:(parameters):参数列表,与函数中的参数列表类似,可以传引用,也可以传值等等等,如果里面没有传递参数的话,可以和()一起省略掉
5: -> return type:显示说明我的返回值的类型是什么,没有返回值,此时可以省略,如果返回值确定,也可以省略,此时编译器回推导返回值是什么。
6: {function boby }:函数体,更普通函数体类似,函数体是不能被省略的
总结:
可以省略的:返回值类型,参数列表
不能够省略的:捕捉列表,函数体
接下来使用几个示例理解一下:
cpp
int main()
{
//一个简单的lambda表达式并实施调用
auto tmp1 = [](int a, int b)->int {return a + b;};//产生一个可调用对象
cout << tmp1(1, 2) << endl;//调用这个可调用对象
//注意哪些是可以省略的(2个可以省略,两个不能省略)
auto tmp2 = []//产生一个可调用对象
{
cout << "hello world" << endl;
};
tmp2();//调用这个可调用对象
return 0;
}

捕捉列表
接下来详细说明捕捉列表的作用,什么场景下需要使用到捕捉列表,捕捉列表的格式问题等:
1:如果lambda表达式只能够使用自己作用域的形参或变量,如果想用外部的形参或是变量,就需要进行捕捉。或者是在lambda表达式中,如果局部域中变量频繁被使用,就可以使用捕捉。因为如果显示写参数,到时候传递的话也麻烦,不如直接捕捉。(不用捕捉全局变量,或是静态局部变量,也不需要捕捉,因为局部作用域下是可以使用全局变量的)
2:捕捉方式1(显示捕捉):值捕捉,引用(&)捕捉。如果同时存在,中间用逗号分隔。值捕捉本质上是拷贝构造(类似于传值传参),不能在lambda表达式中对值捕捉的变量进行修改,可以理解为编译器自己加了一个const
cpp
[a,&b];//值捕捉和引用捕捉(都是显示捕捉)
3:捕捉方式2(隐式捕捉):=/&捕捉,如果需要将需要的变量进行值捕捉/将需要的变量进行引用捕捉。此时使用=/&。注意,不一定是全部变量,只要lambda表达式中需要的变量才会被捕捉。
cpp
[=];//隐式值捕捉
[&];//隐式引用捕捉
4:捕捉方式3:混合捕捉:捕捉方式1和捕捉方式2同时使用。需要满足这些规则:逻辑不能够相矛盾以及顺序问题。
来看一个示例:
cpp
[= , a];//逻辑混乱,本身需要的变量已经使用了值捕捉,后面还跟了一个值捕捉
[&, &a];//逻辑混乱
[a, &];//;逻辑正确,但是顺序错误,隐式捕捉必须放在前面
[&a, =];//顺序错误
[&, =];//逻辑错误,到底是隐式值捕捉还是隐式引用捕捉
总结:混合捕捉隐式捕捉必须在前,而且逻辑需正确,前面是隐式值捕捉,后面只能是引用捕捉,前面是隐式引用捕捉,后面只能是值捕捉。
5:lambda表达式定义在局部域中,只能捕捉lambda表达式之前的变量,因为编译器向上查找的原则。定义在全局域中,不用捕捉任何东西,也不需要捕捉,捕捉列表必须为空。
cpp
int main()
{
int a = 0;
auto tmp1 = [a,b](int a, int b)->int{return a + b;};
int b = 0;//b不能被捕捉,因为可调用函数向上查找找不到b
return 0;
}
6:默认情况值捕捉过来的变量是被const修饰的,不能被修改,如果想让其被修改,可以使用mutable修饰,但是此时参数列表即使为空,()不能被省略。
cpp
int main()
{
int a = 0,b = 1;
auto tmp1 = [a,&b]()mutable//此时虽然没有参数,但是()是不能够省略的。
{
a++;//如果去掉了mutable,这句代码是错的,mutable去掉了a的const
b++;
};
return 0;
}
细节:我虽然在lambda表达式中修改了a,但是因为是值捕捉,是局部域中的a拷贝构造给了lambda表达式中的a。对lambda表达式中的a修改的话是不会影响到外面的。
来看示例:
cpp
int x = 0;
int main()
{
int a = 0, b = 1, c = 2, d = 3;
//显示值捕捉
auto tmp1 = [a](int x, int y)->int
{
//a++;//错误代码,a无法被修改
return a + x + y;
};
cout << tmp1(1, 2) << endl;
//隐式值捕捉,lambda表达式用到的才去捕捉
auto tmp2 = [=]
{
int ret = 0;
ret = a + b + c + d;
return ret;
};
cout << tmp2() << endl;
//混合捕捉
auto tmp3 = [&]
{
a++;
b++;
c++;
d++;
return a + b + c + d;
};
cout << tmp3() << endl;
static int y = 0;
auto tmp4 = []//不能捕捉全局变量和静态局部变量
{
cout << "hello world" << endl;
};
tmp3();
//mutable去除mutable特性
auto tmp5 = [=]()mutable
{
a++;
b--;
c++;
d--;
};
tmp5();
return 0;
}
lambda的应用
在学习 lambda 表达式之前,我们的使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦。使用 lambda 去定义可调用对象,既简单又方便。
比如在算法库中,sort是一个函数模版,需要实现一个可调用对象来进行排序比较,以前我们是使用的仿函数,但是现在可以使用lambda表达式。
来看示例:
cpp
struct Goods{
string _name;
int _evaluatee;//评分
int _price;//价格
}
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3}, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](const Goods& g1,const Goods& g2){return g1._price < g2._price;});
//这里就可以不用再写仿函数传仿函数了,写起来更加方便。
return 0;
}
lambda 在很多其他地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等, lambda 的应用还是很广泛的,以后我们会不断接触到
lambda的原理
1:lambda表达式的底层是仿函数,如果去汇编代码看的话,你会发现和仿函数执行的汇编代码相似。编译器会生成一个类似仿函数的类。
想一下,如果我们都是用lambda表达式,难道编译器生成的类名不可能重复吗,如果重复了,就会造成调用operator()调用的不是我想调的,这该怎么办呢?
编译器底层使用uuid生成字符串作为类名。
这种方式生成相同字符串的概率特别小,比你能中彩票的概率都要小得多,具体可以在网上了解一下uuid,本文不再详细讲解。
2:lambda表达式的参数,返回类型,函数体,就是operator()仿函数的参数,返回类型,函数体。lambda表达式捕捉的本质就是捕捉的内容是operator()的这个类中调用构造函数的实参。即捕获的内容被初始化成员变量的内容了。
来看一下下面的这段代码:
cpp
class Rate{
public:
Rate(double rate)
:_rate(rate)
{}
operator()(double money, int year)
{
return _rate*money*year;
}
private:
double _rate;
};
int main()
{
double rate = 0.025;
auto tmp1 = [rate](double money,int year){
return rate*money*year;
};
tmp1(10000,10);
Rate tmp2(rate);
tmp2(10000,10);
return 0;
}

包装器
我们先来理解一个点,现在c++11中,可调用对象有三者,分别是函数指针,仿函数,lambda表达式。由于三者的类型不同,我们是不能够将三者存储在一个容器中的,但是c++11有了包装器,就能够做到这一点。
function
被包含在头文件< functional>中
注意:上面的那个是一个模版,但是只是声明,而下面的那个是模版特化的语法,但是这里特化和普通模版的特化有点区别。
std::function是一个包装器,也是一个类模版。
std::function 的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调用对象被称为std::function 的目标
为什么function能够包装其他可以调用的对象:
因为function的构造函数重载了函数模版的版本。
我们仔细来查看这个接口,能够发现其中提供了operator接口,如果此时我使用了可调用对象来构造function类,如果再调用operator()接口,就能够直接调用到这个用来构造function类的可调用对象。如果说此时没有使用可调用对象来初始化function,但是调用operator(),调用空std::function 的目标导致抛出std::bad_function_call异常。
function的作用:能够对不同的可调用对象统一它们的类型,方便存取,这样在很多地方就方便声明可调用对象的类型。
我们先来看几个简单的使用function来包装可调用对象:
cpp
int f(int x, int y)
{
return x + y;
}
struct Functor
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
int main()
{
function<int(int, int)> f1(f);
function<int(int, int)> f2 = f;
function<int(int, int)> f3 = Functor();
auto tmp = [](int a, int b) {return a + b;};
function<int(int, int)> f4 = tmp;//此时这三个可调用对象就是同一种类型了,可以将其放入一个容器中
vector<function<int(int, int)>> funcv = { f,Functor(),tmp };
for (auto& e : funcv)
{
cout << e(10, 20) << endl;
}
return 0;
}

function包装可调用对象的规则:类型必须和可调用对象一致(包含参数类型,参数个数)...
模版特化的规则:可调用对象的返回值(可调用对象形参数据类型,...)(对应着个数就可以)
使用function类的时候,难的是去包装成员函数,一定要注意,成员函数是有this指针的,所以一定要注意上类型匹配的问题。
来看下面的代码:
cpp
class Plus
{
public:
Plus(int n = 10)
:_n(n)
{}
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return (a + b) * _n;
}
private:
int _n;
};
int main()
{
function<double(Plus*, double, double)> f1 = &Plus::plusd;
Plus ps;
cout << f1(&ps, 1.1, 2.2) << endl;
function<double(Plus, double, double)> f2 = &Plus::plusd;
cout << f2(ps, 2.2, 3.3) << endl;
function<double(Plus&&, double, double)> f3 = &Plus::plusd;
cout << f3(move(ps), 3.3, 4.4) << endl;
return 0;
}

注意:普通成员函数还有一个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以。
bind
被包含在头文件< functional>中。
bind 是一个函数模板,它也是一个可调用对象的包装器,可以把他看做⼀个函数适配器,对接收的fn可调用对象进行处理后返回一个可调用对象。
bind函数的作用:修改参数的顺序和调整参数个数。
在讲解bind函数作用的时候,我们再来讲解一个类叫placeholders。
形式:auto newCallable = bind(callable,arg_list);
bind也要搭配auto或者function(因为bind也是包装器,可以使用function包装器来接收,但是注意一定类型要匹配)共同使用newCallable:是bind函数返回的可调用对象(而且第一个一定要传这个 )。
callable:是传递的先前已经实现好的可调用对象
arg_list:参数列表(可以手动传递实参,也可写placeholders中的成员变量)。参数列表的顺序最好对应传过来的可调用函数的参数数据类型顺序
placeholders成员变量的作用:
arg_list中的参数可能包含形如_n的名字,其中n是⼀个整数,这些参数是占位符,表示newCallable的参数,它们占据了传递给newCallable的参数的位置。可以理解为:_1始终代表返回的可调用对象的第一个实参,_2始终代表返回的可调用对象的第二个实参,以此类推
来看下例代码:
cpp
int Sub(int a, int b)
{
return (a - b) * 10;
}
int main()
{
//auto sub1 = bind(Sub, 10, 20);//固定了两个参数,此时实参中不需要传参
auto sub1 = bind(Sub, _1, _2);
cout << sub1(1, 2) << endl;
auto sub1 = bind(Sub, _2, _1);
cout << sub1(1, 2) << endl;
return 0;
}

这样通过placeholders就完成了对参数顺序的变化,其实这个效果用的很少很少。
接下来来看参数个数的调整:
cpp
int Sub(int a, int b)
{
return (a - b) * 10;
}
int main()
{
auto sub1 = bind(Sub, 10, 20);//固定了两个参数,此时实参中不需要传参
cout << sub1() << endl;
auto sub2 = bind(Sub, 10, _1);//固定了一个参数,此时实参中只有传一个参数
cout << sub2(10) << endl;
auto sub3 = bind(Sub, _1, 10);//固定了一个参数,此时实参中只有传一个参数
cout << sub3(10) << endl;
return 0;
}
那bind这个用作包装器应用在哪里呢?
在上面,我们是不是发现function包装成员函数的时候由于this指针的影响,导致这个第一个参数传递的是类的指针或者是对象。
所以,这个时候我们绑定第一个类的指针或者是对象,接下来调用可调用对象的时候就不用再来传递类的指针或者是对象了。
来看代码:
cpp
function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);
cout << f7(1.1, 1.1) << endl;
所以:bind一般用于:绑死一些参数
总结
今天我们学习了lambda表达式,新增了可调用对象(三个),学习了lambda的语法格式,每一个部分的特点(应该怎么填,哪些可以省略),最重要的就是捕捉列表。具有显示捕捉,隐式捕捉,混合捕捉。并且学习了lambda的底层就仿函数。随后学习了包装器,学习了function细节是什么,格式是什么,怎么写,为什么有这个(统一可调用变量的类型),学习了bind,作用是什么,如何使用,格式是什么
结语
感谢大家阅读我的文章,不足之处欢迎留言指出,感谢大家支持!!
学而不思则罔,思而不学则殆!!