C++11新增内容详解(三)
一、lambda表达式
1.1基本使用格式
①捕捉列表:编译器通过[]来判断接下来的代码是否为lambda表达式;
可以捕捉上下文变量供lambda使用
②参数列表:函数参数存放地
③mutable:默认lambda表达式核心函数operetor()为const成员函数且捕捉列表中的参数全部属于const修饰,加上这个标识可以取消常量性
使用此修饰符时,参数列表不可省略,返回值的省略不受影响
④返回值类型:没有返回值,返回值类型明确这两种情况可以省略
1.2使用时的问题
①可将lambda表达式看作一个局部匿名函数对象,定义时需要加;
②表达式返回值类型不明确,现阶段只能使用auto来接收
1.3使用举例
1.3.1两数相加
auto add1 = [](int a, int b)->int {return a + b; };
cout << add1(3, 4) << endl;
可以实现将两个参数3,4相加的目的,
之所以可以进行打印,是因为函数的返回值为int类型,可以直接传给operator<<
1.3.2省略参数列表和返回值类型
auto func2 = []
{
cout << "hello world" << endl;
cout << "hello lambda" << endl;
};
func2();
当无参数需要传,无返回值,且没有使用mutable时可以对参数列表和返回值类型进行省略
1.3.3特定情况下代替仿函数,提高代码可读性
假设有一个结构体为Object,两个成员变量name和price表示商品名称和价格
有一个vector对象v,其中存储了o1~o9多个Object类型对象
现在要对v进行按价格排序
sort(v.begin(),v.end(),[](const Object& o1,const Object& o2)->bool
{
return o1.price < o2.price;//排价格升序
});
在函数参数列表中就可以写上lambda表达式
1.4捕捉列表相关问题
1.4.1捕捉列表的特点
默认使用传值捕捉,即将要捕捉的对象进行一次拷贝;如果不希望造成这一消耗可以传引用捕捉
1.4.2结合例子理解
假设此时的需求是:交换int类型变量a,b的值
已有a=0,b=1;
①不用捕捉列表,参数列表直接给int&
//由于返回值类型是确定的,所以省略
auto swap1 = [](int& a1, int& b1)
{
int tmp = a1;
a1 = b1;
b1 = tmp;
};
swap(a, b);
在没有进行捕捉的时候,只能用当前lambda局部域对象和全局域对象
②捕捉列表用传值捕捉(错误示例)
auto swap2 = [a,b]()mutable
{
int tmp = a;
a = b;
b = tmp;
};
swap2();
这是无法完成交换的,传值捕捉的本质是一种拷贝,并且被const修饰了
(mutable则为去掉const属性来实现将两个变量的值进行互换)
③引用捕捉进行修改
auto swap3 = [&a, &b]()
{
int tmp = a;
a = b;
b = tmp;
};
swap3();
其中 “[&a, &b]”的写法不是取地址,而是进行引用捕捉
1.4.3引用列表中的特定符号
①[=] 可以实现所有父作用域的值全部传值捕捉
②[&]可以实现所有父作用域的值全部传引用捕捉
③混合捕捉:
[=,&d] d传引用捕捉,其他传值捕捉
[&,d] d传值捕捉,其他传引用捕捉
1.4补
① mutable默认传值为const修饰,就是防止误用导致的错误
②全局变量无需捕捉
1.5lambda表达式的底层实现
1.5.1实际类型
lambda表达式的实际类型为:名为lambda_(uuid)的类模板
1.5.1补:什么是uuid
uuid:唯一通用识别码,是一串不重复的字符串,
即使是相同的实现过程,两个不同lambda表达式的uuid也是不同的,
因此,我们不能明确lambda表达式的类型,只能通过auto推导
1.5.2定义的过程和捕捉的本质
定义的过程实际上就是调用构造,捕获的对象作为类成员变量,而捕捉的本质就是构造函数的初始化参数
类比于调用类的成员函数:实现一个AD类,调用其成员函数add
而lambda表达式
综上,lambda表达式本质上也是在调用函数 ,都经历开空间->压栈->计算->返回的过程
那么lambda表达式本质上在调用的是什么函数呢?
正是operator()这一仿函数
1.5.3全部捕捉时的性能改善
全部捕捉[=]时,编译器会自动推导接下来会用到的变量,然后只对这些进行捕获,从而加速你好拷贝消耗
⭐1.5.4调用过程的本质
根据1.5.3中介绍,调用过程的本质就是调用lambda_(uuid)类的operator()成员函数
二、包装器
2.1基础模板与作用
包装器最常用的是function,用来封装任意类型的可调用对象,因此,function的主要作用便是:
统一可调用对象
(常见的如①函数指针②仿函数③lambda表达式)
模板:
template <class T> function; // undefined template <class Ret, class... Args> class function<Ret(Args...)>;
其中的Ret模板参数表示返回值类型,这也造就了其独特的使用方式
2.2使用举例
使用前需要包头文件
#include<functional>
提前准备一个函数与一个仿函数
//函数指针
int func1(int a, int b) { return a + b; }
//仿函数
struct FUNC2 {
int operator()(int a1,int b1)
{
return a1 + b1;
}
};
之后在main函数中调用
//函数指针
function<int(int, int)> f1 = func1;
//仿函数
function<int(int, int)> f2 = FUNC2();
//lambda表达式
function<int(int, int)> f3 = [](int a, int b)->int {return a + b; };
cout << f1(2,5) << endl;
cout << f2(3,5) << endl;
cout << f3(4,5) << endl;
根据例子可以得出结论:在通过function进行封装以后,不论这一个可调用对象一开始是什么,都可以用唯一一种调用方式来进行使用
(...................................................35.....................7.31...................................................................)
2.3特殊情况:包装类的成员函数指针
假设有这样一个类
class Func_3 {
public:
static int Func_3_1(int a, int b)
{
return a + b;
}
double Func_3_2(double c, double d)
{
return c + d;
}
};
其中有两种成员函数,我们分别进行讨论
2.3.1包装静态成员函数指针
成员函数按照要求必须要在函数名前加上“&”来表示使用的是成员函数的地址,其次使用时需要指明类域
除这些最基本的之外与普通函数没有任何区别
function<int(int, int)> f4 = &Func_3::Func_3_1;
cout << f4(9, 7) << endl;
2.3.2包装非静态成员函数指针
要包装非静态成员函数有一个很关键的问题:隐含参数this指针
要解决这一问题有两种方式
①方式一:定义新对象,该对象仅用于提供this指针充位
Func_3 nf3;
function<double(Func_3*,double, double)> f5 = &Func_3::Func_3_2;
cout << f5(&nf3, 9.1, 7.6) << endl;
其实在底层上,“(&nf3, 9.1, 7.6)”这一参数列表并不是直接提供给成员函数Func_3_2的,
而是通过nf3对象来调用它的成员函数Func_3_2,所以我们实际上无需传入类指针,只要传入类编译器就可以自动推导
function<double(Func_3, double, double)> f6 = &Func_3::Func_3_2;
cout << f6(nf3, 9.2, 7.6) << endl;
②方式二:直接使用匿名对象
其实是对①方式在效率上的改进
在f6定义后,直接使用
cout << f6(Func_3(), 9.5, 7.6) << endl;
2.4除了function外,还有第二种包装器bind
2.4.1bind是什么
bind是一个函数模板,属于std命名空间
无返回值:
template <class Fn, class... Args> /* unspecified */ bind (Fn&& fn, Args&&... args);
有返回值:
template <class Ret, class Fn, class... Args> /* unspecified */ bind (Fn&& fn, Args&&... args);
传入的是一个可调用对象和一个特定的可变模板参数 返回值为可调用对象(可能性很多所以并不明确)
其中的特定的可变模板参数为命名空间placeholders 中的变量
namespace placeholders { extern /* unspecified */ _1; extern /* unspecified */ _2; extern /* unspecified */ _3; // ... }
对应的_1,_2,_3...为调用bind返回值对象时,传入的参数顺序
2.4.2结合例子理解使用方法
bind主要有两个用途:
①调整参数顺序
②调整参数个数
基础使用:
using placeholders::_1;
using placeholders::_2;
int sub(int a, int b)
{
return (a - b) * 120;
}
int main()
{
auto sub1 = bind(sub, _1, _2);
cout << sub1(10, 5) << endl;
return 0;
}
代码解析:
功能一:
//作用1:调整参数顺序
auto sub2 = bind(sub, _2, _1);
cout << sub2(10, 5) << endl;//输出-600
功能二:
//作用2:调整参数个数
auto sub3 = bind(sub, 100, _1);
cout << sub3(6) << endl;//输出11280
auto sub4 = bind(sub, _1, 100);
cout << sub4(6) << endl;//输出-11280
不难发现:函数指针之后的几个位置分别对应函数的参数列表中对应的位置
如果调用bind对象只传一个参数,那么_2无论如何也用不上
2.4.3实用场景
①2.3.2中的包装可以进行简化了
可以用bind来绑死this指针位置的参数
//原先:
function<double(Func_3, double, double)> f6 = &Func_3::Func_3_2;
cout << f6(nf3, 9.2, 7.6) << endl;
cout << f6(Func_3(), 9.5, 7.6) << endl;
//用了bind以后:
function<double(double, double)> f7 = bind(&Func_3::Func_3_2, Func_3(),_1,_2) ;
cout << f7(9.2, 7.6) << endl;
②利息的计算:bind绑定lambda表达式
function<double(double)> interest = bind([](double rate,
double money, int year)->double
{
return rate * money * year;
}, 0.015, _1, 10);
cout << interest(10000) << endl;
目的是我们只需要传入money的数量,就可以计算1.5%利率下存十年的利息,
function的类型因此只需要返回值类型double和传入值类型double