【C++进阶】C++11 的新特性 | lambda | 包装器
👓️博主简介:
文章目录
- 前言
- 一、lambda
- 1.1、lambda 表达式语法
- 1.2、捕捉列表
- 1.3、lambda 的应用
- 1.4、lambda 的原理
- 二、包装器
- 2.1、function
- 2.2、bind
- 总结
前言
我们目前已经学习了蛮多 C++11 的新特性了,我们再来了解了解我们的 lambda 和 包装器,它们也是十分有用的工具,我们赶紧一起来看看吧。
一、lambda
1.1、lambda 表达式语法
- lambda 表达式本质是一个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda 表达式语法使用层而言没有类型,所以我们一般是用 auto 或者模板参数定义的对象去接收 lambda 对象
- lambda 表达式的格式: [capture-list] (parameters)-> return type { function boby }
- [capture-list]:捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据 [ ] 来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉,具体细节 1.2 中我们再细讲。捕捉列表为空也不能省略
- (parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同 ( ) 一起省略
- ->return type:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导
- { function boby }:函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略
// ⼀个简单的lambda表达式
auto add1 = [](int x, int y)->int {return x + y; };
cout << add1(1, 2) << endl;
我们可以看到,当我们调用这个 lambda 表达式时,非常像调用一个函数。
// 1、捕捉为空也不能省略
// 2、参数为空可以省略
// 3、返回值可以省略,可以通过返回对象⾃动推导
// 4、函数题不能省略
auto func1 = []{cout << "hello world" << endl;return 0;};
func1();
我们的捕捉列表和函数体不能省略,返回值和参数如果没有就可以被省略。比如我们写一个可以打印 hello world 的 lambda,就没有参数和返回值,我们就可以把它们都省略掉。
我们还可以用 lambda 实现一个交换。
int a = 0, b = 1;
auto swap1 = [](int& x, int& y){int tmp = x;x = y;y = tmp;};
swap1(a, b);
cout << a << ":" << b << endl;
写法和我们的交换函数是差不多的。
1.2、捕捉列表
- lambda 表达式中默认只能用 lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉
// 只能用当前lambda局部域和捕捉的对象和全局对象
int a = 0, b = 1, c = 2, d = 3;
auto func1 = [a, &b]{b++;int ret = a + b;return ret;};
此时我们的 a 和 b 就是外部变量,我们必须要捕捉才能使用,如果不捕捉就会报错。
- 第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[x,y, &z] 表示 x 和 y 值捕捉,z 引用捕捉。
我们上面的 a 就是传值捕捉, 而 b 就是传引用捕捉。同理,我们的值捕捉的变量不能修改,而引用捕捉的变量可以修改。
int a = 0, b = 1, c = 2, d = 3;
auto func1 = [a, &b]{// 值捕捉的变量不能修改,引⽤捕捉的变量可以修改//a++;b++;int ret = a + b;return ret;};
cout << func1() << endl;
cout << a << ":" << b << endl;
- 第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写一个 = 表示隐式值捕捉,在捕捉列表写一个 & 表示隐式引用捕捉,这样我们 lambda 表达式中用了那些变量,编译器就会自动捕捉那些变量。
// 隐式值捕捉
// 用了哪些变量就捕捉哪些变量
auto func2 = [=]{int ret = a + b + c;return ret;};
cout << func2() << endl;// 隐式引用捕捉
// 用了哪些变量就捕捉哪些变量
auto func3 = [&]{a++;c++;d++;};
func3();
cout << a << " " << b << " " << c << " " << d << endl;
- 第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[ =, &x ] 表示其他变量隐式值捕捉,x 引用捕捉;[ &, x, y ] 表示其他变量引用捕捉,x 和 y 值捕捉。当使用混合捕捉时,第一个元素必须是 & 或 =,并且 & 混合捕捉时,后面的捕捉变量必须是值捕捉,同理 = 混合捕捉时,后面的捕捉变量必须是引用捕捉。
auto func4 = [&, a, b]{//a++; // 由于是值捕捉,所以不//b++; // 能加减,否则报错c++;d++;return a + b + c + d;};
func4();
cout << a << " " << b << " " << c << " " << d << endl;
// 混合捕捉2
auto func5 = [=, &a, &b]{a++;b++;//c++; // 由于是值捕捉,所以不//d++; // 能加减,否则报错return a + b + c + d;};
func5();
cout << a << " " << b << " " << c << " " << d << endl;
- lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
int x = 0;
int main()
{// 局部的静态和全局变量不能捕捉,也不需要捕捉static int m = 1;auto func6 = []{int ret = x + m;return ret;};cout << func6() << endl;
}
- 默认情况下,lambda 捕捉列表是被 const 修饰的,也就是说传值捕捉的过来的对象不能修改,mutable 加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。
// 传值捕捉本质是⼀种拷贝,并且被 const 修饰了
// mutable 相当于去掉 const 属性,可以修改了
// 但是修改了不会影响外⾯被捕捉的值,因为是⼀种拷贝
auto func7 = [=]()mutable{a++;b++;c++;d++;return a + b + c + d;};
cout << func7() << endl;
cout << a << " " << b << " " << c << " " << d << endl;
这里外面的 a、b、c、d、没有改变,只有 lambda 内部发生改变。
1.3、lambda 的应用
在学习 lambda 表达式之前,我们的使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦。使用 lambda 去定义可调用对象,既简单又方便。
lambda 在很多其他地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等, lambda 的应用还是很广泛的,以后我们会不断接触到。
struct Goods
{string _name; // 名字double _price; // 价格int _evaluate; // 评价// ...Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};struct ComparePriceLess
{bool operator()(const Goods& gl, const Goods& gr){return gl._price < gr._price;}
};struct ComparePriceGreater
{bool operator()(const Goods& gl, const Goods& gr){return gl._price > gr._price;}
};int main()
{vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3}, { "菠萝", 1.5, 4 } };// 类似这样的场景,我们实现仿函数对象或者函数指针⽀持商品中// 不同项的比较,相对还是比较麻烦的,那么这⾥ lambda 就很好用了sort(v.begin(), v.end(), ComparePriceLess());sort(v.begin(), v.end(), ComparePriceGreater());sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price < g2._price;});sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price > g2._price;});sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._evaluate < g2._evaluate;});sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._evaluate > g2._evaluate;});return 0;
}
此时如果我们去实现仿函数,或者说旁边有注释说明这个仿函数的含义还好,如果我们在使用排序的逻辑时,既不是自己写的逻辑,也没有写注释,那就很麻烦了,我们就得去找这个仿函数的含义到底是什么。此时我们直接使用 lambda 表达式,让人一眼就能知道逻辑是什么,此时这串代码就可以省去别人去各个文件中寻找的时间了。
1.4、lambda 的原理
-
lambda 的原理和范围 for 很像,编译后从汇编指令层的角度看,压根就没有 lambda 和范围 for 这样的东西。范围 for 底层是迭代器,而lambda 底层是仿函数对象,也就说我们写了一个 lambda 以后,编译器会生成一个对应的仿函数的类。
-
仿函数的类名是编译按一定规则生成的,保证不同的 lambda 生成的类名不同,lambda 参数/返回类型/函数体就是仿函数 operator() 的参数/返回类型/函数体, lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。
// 仿函数
class Rate
{
public:Rate(double rate): _rate(rate){}double operator()(double money, int year){return money * _rate * year;}
private:double _rate;
};
// lambda
double rate = 0.49;
auto r2 = [rate](double money, int year) {return money * rate * year;};
我们尝试调用上面的仿函数和 lambda。
Rate r1(rate);
cout << r1(10000, 2) << endl;
cout << r2(10000, 2) << endl;
本质上它们是一样的。
二、包装器
2.1、function
- std::function 是一个类模板,也是一个包装器。std::function 的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、 lambda、bind 表达式等,存储的可调用对象被称为 std::function 的目标。若 std::function 不含目标,则称它为空。调用空 std::function 的目标导致抛出 std::bad_function_call 异常。function 被定义 < functional > 头⽂件中。
int f(int a, int b)
{return a + b;
}
struct Functor
{
public:int operator() (int a, int b){return a + b;}
};
在我们的 function 中,( ) 里保存的是参数类型,而 <> 里 ( ) 外保存的就是返回值类型。
// 包装各种可调⽤对象
function<int(int, int)> f1 = f;
function<int(int, int)> f2 = Functor();
function<int(int, int)> f3 = [](int a, int b) {return a + b; };
cout << f1(1, 1) << endl;
cout << f2(1, 1) << endl;
cout << f3(1, 1) << endl;
我们第一个包装器想要保存 f。它是一个返回值为 int,参数为 int,int。所以包装器的写法就是 <int (int, int)>。而第二个也是如此,它的调用方式也和第一个是一样的。只要我们的可调用类型和我们包装器的参数是匹配的,就可以被包装,否则就会报错。
除了全局函数,我们还可以包装一个静态成员函数,但是与全局不同的地方在于,静态成员函数的包装需要指定类域。
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;
};
// 包装静态成员函数
// 成员函数要指定类域并且前⾯加&才能获取地址
function<int(int, int)> f4 = &Plus::plusi;
cout << f4(1, 1) << endl;
我们这里除了指定类域,前面还加了一个 &,这是因为规定非静态的成员函数想要包装必须前面加一个 &,但是静态成员函数可加可不加,但是建议还是加。
// 包装普通成员函数
// 普通成员函数还有⼀个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以
function<double(Plus*, double, double)> f5 = &Plus::plusd;
Plus pd;
cout << f5(&pd, 1.1, 1.1) << endl;
注意我们的成员函数前面默认有一个 this 指针,为了参数匹配我们也得加一个指针在前面。
除了上面的写法,我们还可以这样包装成员函数。
function<double(Plus, double, double)> f6 = &Plus::plusd;
Plus pd;
cout << f6(pd, 1.1, 1.1) << endl;
成员函数是用一个 .* 运算符调用的,它会显示传递对象的地址,如果是指针就直接传递,如果是对象它会自动取地址。
也可以用右值引用。
function<double(Plus&&, double, double)> f7 = &Plus::plusd;
Plus pd;
cout << f7(move(pd), 1.1, 1.1) << endl;
cout << f7(Plus(), 1.1, 1.1) << endl;
这些写法都是可以的,大家看着使用即可。
- 函数指针、仿函数、 lambda 等可调用对象的类型各不相同, std::function 的优势就是统一类型,对他们都可以进行包装,这样在很多地方就方便声明可调用对象的类型。
2.2、bind
-
bind 是一个函数模板,它也是⼀个可调用对象的包装器,可以把他看做一个函数适配器,对接收的 fn 可调用对象进行处理后返回一个可调用对象。 bind 可以用来调整参数个数和参数顺序。bind 也在 < functional > 这个头文件中。
-
调用 bind 的一般形式: auto newCallable = bind(callable,arg_list); 其中 newCallable 本身是一个可调用对象,arg_list 是一个逗号分隔的参数列表,对应给定的 callable 的参数。当我们调用 newCallable 时,newCallable 会调用 callable,并传给它 arg_list 中的参数。
-
arg_list 中的参数可能包含形如 _n 的名字,其中 n 是一个整数,这些参数是占位符,表示 newCallable 的参数,它们占据了传递给newCallable 的参数的位置。数值 n 表示生成的可调用对象中参数的位置:_1 为 newCallable 的第一个参数,_2 为第二个参数,以此类推。_1/_2/_3… 这些占位符放到 placeholders 的⼀个命名空间中。
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int Sub(int a, int b)
{return (a - b) * 10;
}
int SubX(int a, int b, int c)
{return (a - b - c) * 10;
}
class Plus
{
public:static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return a + b;}
};
首先,这个用法可以调整参数的顺序。
auto sub1 = bind(Sub, _1, _2);
cout << sub1(10, 5) << endl;
auto sub2 = bind(Sub, _2, _1);
cout << sub2(10, 5) << endl;
第一个参数传的是可调用对象,后面传的就是占位符,_1 代表第一个参数,_2 代表第二个参数。它的返回值是一个仿函数,可以用 auto 或者 function 接收。
其次它还可以用来调整参数个数。
// 调整参数个数 (常⽤)
auto sub3 = bind(Sub, 100, _1);
cout << sub3(5) << endl;
auto sub4 = bind(Sub, _1, 100);
cout << sub4(5) << endl;
// 分别绑死第123个参数
auto sub5 = bind(SubX, 100, _1, _2);
cout << sub5(5, 1) << endl;
auto sub6 = bind(SubX, _1, 100, _2);
cout << sub6(5, 1) << endl;
auto sub7 = bind(SubX, _1, _2, 100);
cout << sub7(5, 1) << endl;
我们想把一个参数固定死,我们只要在对应位置输入我们要固定的值,那那个位置就会被固定死了。剩下的位置我们就会重新分配占位符,还是从 _1 开始,但是以 N(总参数数量) - M(绑定数数量)为结尾。
我们可以利用这个 bind 把上面的包装器优化一下。
function<double(Plus&&, double, double)> f6 = &Plus::plusd;
Plus pd;
cout << f6(move(pd), 1.1, 1.1) << endl;
cout << f6(Plus(), 1.1, 1.1) << endl;
// bind⼀般⽤于,绑死⼀些固定参数
function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);
cout << f7(1.1, 1.1) << endl;
我们把我们的 this 指针的位置绑定死,这样就没有必要每次都传了。
总结
以上便是我们 C++11 的一些比较有用的内容和知识点了,我们一定要对其认真学习了解,下一章节我们来了解了解我们的异常是什么,我们下一章节再见。
🎇坚持到这里已经很厉害啦,辛苦啦🎇 ʕ • ᴥ • ʔ づ♡ど