目录
lambda表达式
引入
lambda表达式介绍
lambda表达式捕捉列表的传递形式
lambda表达式的原理
包装器
包装器的基本使用
包装器与重载函数
包装器的使用
绑定
C++ 11 新特性
lambda表达式
引入
在C++ 98中,对于sort
函数来说,如果需要根据不同的比较方式实现不同的排序结果,需要写不同的仿函数,而在C++ 11中,可以通过lambda表达式解决这个问题,例如下面的例子:
struct Goods
{string _name; // 名字double _price; // 价格int _evaluate; // 评价Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};
当需要按照商品的价格和评价排序时,则需要写两个仿函数
struct ComparePrice
{// 按照价格排序bool operator()(const Goods& g1, const Goods& g2){return g1._price < g2._price;}
};struct CompareEvaluate
{// 按照评价排序bool operator()(const Goods& g1, const Goods& g2){return g1._evaluate < g2._evaluate;}
};
调用时传递仿函数匿名对象
sort(v.begin(), v.end(), ComparePrice());
sort(v.begin(), v.end(), CompareEvaluate());
但是当需要按照其他方式进行比较时,需要再写其他的仿函数,为了简化步骤,可以使用lambda表达式
lambda表达式介绍
lambda表达式基本结构如下:
[捕捉列表](形式参数)mutable->返回值类型
{函数体
}
- 捕捉列表:编译器根据
[]
来判断接下来的代码是否为lambda函数,用于传递在lambda表达式体内的使用到的参数,一般为lambda表达式所在的直接作用域的变量 - 形式参数:用于lambda表达式体内的变量,如果不需要传递形式参数,则当前项可以省略不写,如果需要加
mutable
,则不论是有还是没有形式参数,都需要带上()
mutable
:默认情况下lambda表达式捕捉列表的参数是被const
修饰的,所以捕捉列表的参数是以传值的方式传递时是无法直接在lambda表达式内部进行修改的,但是如果加了mutable
,就可以取消const
属性->
返回值类型:与普通的函数体一样的返回类型声明,如果lambda表达式的返回值类型比较明确时,该项可以不写- 函数体:同普通函数
有了lambda表达式,引入部分的例子中的仿函数可以用lambda表达式进行替换,如下:
// lambda表达式
sort(v.begin(), v.end(), [](Goods& g1, Goods& g2){return g1._price < g2._price;});
sort(v.begin(), v.end(), [](Goods& g1, Goods& g2){return g1._evaluate < g2._evaluate;});
lambda表达式捕捉列表的传递形式
如果没有形式参数传递,lambda表达式想使用其所在的直接作用域中的变量(全局除外)需要在捕捉列表中传递,在lambda表达式中,捕捉列表的传递形式一共有4种:
- 具体变量值传递
[variable]
:直接传递变量的值,在lambda表达式中就是对该变量的值进行拷贝,所以lambda表达式内部对variable
修改时不影响variable
本身的内容,并且在没有mutable
的情况下不可以在内部对variable
进行修改 - 具体变量引用传递
[&variable]
:以variable
引用的方式传递,在lambda表达式中可以对variable
内容进行修改,从而达到传址调用的效果 - 所有变量值传递
[=]
:将lambda表达式所在作用域中的变量全部以传值的方式传递给lambda表达式,具体传递了哪些值需要看lambda表达式中使用到了哪些值 - 所有变量引用传递
[&]
:将将lambda表达式所在作用域中的变量全部以传址的方式传递给lambda表达式,具体传递了哪些值需要看lambda表达式中使用到了哪些值,如果lambda内部需要进行修改,需要加mutable
对于第二种情况,如果想在lambda表达式内部修改lambda所在作用域的变量的值,可以在lambda表达式的形式参数部分以引用的方式传递实参,此时就可以不需要添加 mutable
关键字,这个方法与普通函数的思路一致
上面4种方法也可以交错使用,例如下面的代码:
int main()
{int a = 0;int b = 0;int c = 0;// a,b以值传递,c以引用传递auto func = [=, &c](){// a和b是值传递,不能修改// a = 10;// b = 20;// c是引用传递,可以修改c = 30;};func();return 0;
}
lambda表达式的原理
前面通过lambda表达式简化了原本应该使用仿函数改写比较方式的例子展示了lambda表达式的使用,但是lambda表达式实际与仿函数的原理基本一致,所以lambda表达式也被称为匿名函数,观察下面的代码的反汇编代码
class Rate
{
public:Rate(double rate): _rate(rate){}double operator()(double money, int year){return money * _rate * year;}
private:double _rate;
};int main()
{// 创建普通对象double rate = 0.1;Rate r(rate);// 使用仿函数r(10000, 2);// 使用lambda表达式auto func = [=](double money, int year){return money * rate * year;};func(10000, 2);return 0;
}
反汇编如下:
需要注意的是,lambda表达式对象不可以相互转化,尽管完全相同,在底层两个逻辑一模一样的lambda表达式存在不同的lambda+uuid
名称
包装器
包装器的基本使用
C++11中引入了function
包装器,也叫做适配器,在前面有了lambda表达式后,可以发现如果直接调用lambda表达式的对象,其方式和函数的调用基本相同,但是前面的函数还有可能是仿函数,为了使程序的模版使用效率变高,可以使用包装器
使用包装器需要引入头文件 <functional>
包装器可以根据已有的函数、函数指针、lambda表达式进行包装,基本结构如下:
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;// 其中Ret代表指定的函数的返回值
// Args代表指定的函数的参数
需要注意,使用包装器必须保证包装器中的模版参数(返回值和形式参数)与被包装的对象完全相同
使用方式如下:
#include <functional>class Rate
{
public:Rate(double rate) : _rate(rate){}double operator()(double money, int year){return money * _rate * year;}static double calculate(double money, int year){return money * 0.1 * year;}
private:double _rate;
};double func(double money, int year)
{return money * 0.1 * year;
}int main()
{// 包装普通函数function<double(double, int)> func1 = func;func1(10000, 2);// 包装仿函数function<double(Rate, double, int)> func2 = &Rate::operator();func2(Rate(0.1), 10000, 2);function<double(double, int)> func3 = Rate::calculate;func3(10000, 2);// 包装lambda表达式function<double(double, int)> func4 = [=](double money, int year){return money * 0.1 * year;};func4(10000, 2);return 0;
}
上面代码中,包装普通函数与包装函数指针类似,包装成员函数需要注意两种形式:1. 静态成员函数 2. 非静态成员函数,对于静态成员函数来说,可以直接取地址,与普通函数类似,但是需要指定类域,而对于非静态成语函数来说,需要加上&
,因为非静态成员函数存在一个隐含的参数this
,需要依赖对象实例进行调用,所以非静态成员需要传递一个类名(或类指针)代表调用时需要传递同类类型的(匿名/非非匿名)对象(或对象地址),在调用包装后的函数时,实际上是通过对象进行调用,而不是包装器进行调用,对于lambda表达式来说,直接使包装器接受lambda表达式即可
包装器与重载函数
重载函数的根本条件就是必须满足函数名相同,但是此时如果直接使用函数名作为包装器的对象就会产生二义性问题,例如下面的代码:
#include <functional>
int add(int a, int b)
{return a + b;
}double add(double a, double b)
{return a + b;
}int main()
{map<int, function<int(int, int)>> m;m.insert({ 1, add });return 0;
}报错信息:
'std::_Tree<std::_Tmap_traits<_Kty,_Ty,_Pr,_Alloc,false>>::insert': no overloaded function could convert all the argument types
在上面的代码中,map的模版参数是int
和function<int(int, int)>
,代码中也存在对应包装器模版类型的add
函数,但是编译器并不会自动选择对应的重载函数,所以在出现重载函数时,推荐使用函数指针对重载函数进行指代,再传入函数指针,避免传入重载函数的函数名,另外也可以使用lambda表达式,从而不使用函数重载,例如下面的代码:
int main()
{map<int, function<int(int, int)>> m;//m.insert({ 1, add }); 直接插入导致二义性// 使用函数指针指代需要插入的函数int (*pint)(int, int) = add;m.insert({ 1, pint });// 使用lambda表达式m.insert({ 2, [](int a, int b) {return a + b; } });return 0;
}
包装器的使用
C++形式的转移表,以实现简易计算器为例:
下面的代码是用于计算的函数:
int add(int a, int b)
{return a + b;
}int sub(int a, int b)
{return a - b;
}int divide(int a, int b)
{return a / b;
}int multiply(int a, int b)
{return a * b;
}
常规写法:
int main()
{// 处理操作数和操作符输入int num1 = 0;int num2 = 0;char opt = 0;int flag = 1;// 处理计算while (flag && cin >> opt){switch (opt){case '+':cin >> num1 >> num2;cout << add(num1, num2) << endl;break;case '-':cin >> num1 >> num2;cout << sub(num1, num2) << endl;break;case '*':cin >> num1 >> num2;cout << multiply(num1, num2) << endl;break;case '/':cin >> num1 >> num2;cout << divide(num1, num2) << endl;break;default:flag = 0;break;}if (flag == 0){break;}}return 0;
}
使用包装器后:
#include <functional>int main()
{map<char, function<int(int, int)>> m{ {'+', add}, {'-', sub}, {'*', multiply}, {'/', divide} };int num1 = 0;int num2 = 0;char opt = 0;while (cin >> opt){if (m.count(opt)) // 如果count为1,代表map中存在对应的键值对{cin >> num1 >> num2;cout << m[opt](num1, num2) << endl;// 返回的value是包装器,直接传参即可调用对应}else{break;}}return 0;
}
绑定
在C++ 11中,增加了绑定配合包装器的使用,包装器可以实现两种功能:
- 改变实参在传参时的顺序
- 固定形参中的某一个值
使用bind
时需要展开命名空间placeholders
,因为要使用其中的_1
,_2
...
placeholders
中的内容表示调用绑定函数的实际参数,_1
代表第一个实际参数,_2
代表第二个实际参数,以此类推
- 改变实参在传参时的顺序
#include <functional>
using namespace placeholders;int sub(int a, int b, int c)
{return a - b - c;
}int main()
{// 1. 改变实际参数顺序function<int(int, int, int)> func = sub;cout << func(1, 2, 3) << endl;// 绑定改变顺序func = bind(func, _2, _3, _1);// 改变后的传递顺序为:2, 3, 1cout << func(1, 2, 3) << endl;return 0;
}输出结果:
-4
-2
传递顺序改变如下图所示:
注意,bind
改变的是实际参数的传递顺序,而不是形参的接收顺序,形参接收还是按照从左到右依次接收传递的实际参数,只是写的第一个实际参数(本应该传递给形参a)被bind
改变作为第三个实际参数,传递给形参c
,依次类推a
和b
- 固定形参中的某一个值
在前面使用function
包装器调用非静态成员函数时,需要单独传递一个对象给隐含的this
指针,如果每一次传递都需要传递一个对象会显得繁琐,可以考虑将对象参数进行固定,例如下面的代码:
class Rate
{
public:Rate(double rate): _rate(rate){}double calculate(double money, int year){return money * _rate * year;}private:double _rate;
};int main()
{// 不使用bind下使用包装器function<double(Rate, double, int)> func1 = &Rate::calculate;cout << func1(Rate(0.1), 10000, 2) << endl;// 使用bind下使用包装器function<double(Rate, double, int)> func2 = &Rate::calculate;// 使用bind固定对象Rate(0.1)function<double(double, int)> func3 = bind(func2, Rate(0.1), _1, _2);cout << func3(10000, 2) << endl;return 0;
}输出结果:
2000
2000
上面代码中,需要注意尽管固定了func2
的第一个参数,实际参数的指代还是从_1
开始,如果固定中间的参数,则最左边的为_1
,最右边的为_2
(代码如下),以此类推
function<double(Rate, int)> func4 = bind(func2, _1, 10000, _2);
cout << func4(Rate(0.1), 2) << endl;