目录
1. lambda表达式
1.1 引入原因
C++11之前,如果想要对一个数组排序,可以使用algorithm算法库中sort排序函数。一般情况下,排序的结果是升序的。因为默认使用less类中'()'运算符重载函数,此函数内的比较规则是返回小于比较的结果。
如果想要降序,需要改变元素比较规则,会使用到greater类。在sort函数传参时,传greater类匿名对象,就可以达到降序的效果
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void PrintVector(const vector<int> v)
{
for (const auto& e : v)
{
cout << e << " ";
}
cout << endl;
}
int main()
{
vector<int> v1= { 3,1,8,7,4,5,6,2,0,9 };
// 默认按照小于<比较,排出来结果是升序
sort(v1.begin(), v1.end());
PrintVector(v1);
vector<int> v2 = { 3,1,8,7,4,5,6,2,0,9 };
// 如果需要降序,需要改变元素的比较规则,传greater仿函数匿名对象
sort(v2.begin(), v2.end(), greater<int>());
PrintVector(v2);
return 0;
}
运行结果如下:
如果待排序对象是自定义类型,需要用户定义排序比较的规则。如下面所示,定义一个学生类,包含姓名,年龄和成绩三个成员变量。如果想按照年龄排升序,得自己实现一个类重载()函数。如果想按照成绩排降序,也需要自己实现。
struct Student
{
string _name; //姓名
int _age; //年龄
float _grade; //成绩
Student(string name, int age, float grade)
:_name(name)
,_age(age)
,_grade(grade)
{}
};
struct CompareAgeLess //年龄升序
{
bool operator()(const Student& s1, const Student& s2)
{
return s1._age < s2._age;
}
};
struct CompareGradeGreater //成绩降序
{
bool operator()(const Student& s1, const Student& s2)
{
return s1._grade > s2._grade;
}
};
void PrintVector(const vector<Student> v)
{
for (auto& e : v)
{
cout << e._name << " ";
}
cout << endl;
}
int main()
{
vector<Student> v = { {"张三", 22, 84.9}, {"李四", 20, 88.4}, {"王五", 23, 86.7} };
sort(v.begin(), v.end(), CompareAgeLess());
PrintVector(v);
sort(v.begin(), v.end(), CompareGradeGreater());
PrintVector(v);
return 0;
}
运行结果如下:
随着C++的发展,人们开始觉得上面的写法有点复杂。为了实现一个sort算法,需要重新写一个类。并且每次比较逻辑不同,需要实现多个类,带来了极大的不便。因此,C++11语法中引进了Lambda表达式。
1.2 lambda表达式
lambda表示式的格式:[capature-list] (parameters) mutable -> return-type {statement}
- 捕获列表(capture list):该列表总是出现在lambda函数的开始位置,指定了lambda表达式可以访问哪些外部变量。
- 参数列表(parameters):与普通函数的参数列表相同,指定了lambda函数的参数。如果不需要参数传递,则可以连通()一起省略。
- mutable关键字:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略。
- 可返回类型(->return type):lambda表达式的返回类型,没有返回值时可以省略。返回值类型明确情况下,也可省略,编译器会根据返回值自动推断。
- 函数体({statement}):该函数体内,除了可以使用传递的参数外,还可以使用所捕获的变量。
我们先写几个简单的lambda函数。
- 第一个是两数相加的函数,捕获列表我们暂时不用。除了函数名没写,其他函数的部分都存在。我们创建一个对象,使用auto自动推导类型,接收这个函数,就可以像函数一样调用它。
- 第二个函数是打印几句话,因为不要传递参数,也没有返回值,可以省略掉“()->”。
- 第三个函数有返回值,也可以省略返回值类型的部分,编译器可以自动推导。
int main()
{
auto Add1 = [](int x, int y)->int { return x + y; };
cout << Add1(10, 20) << endl;
auto Fun1 = []
{
cout << "hello world" << endl;
cout << "hello world" << endl;
cout << "hello world" << endl;
};
Fun1();
//可以进行自动推导
auto Fun2 = []
{
cout << "hello world" << endl;
cout << "hello world" << endl;
cout << "hello world" << endl;
return 0;
};
Fun2();
return 0;
}
运行结果如下。不过还是建议将返回值类型写上,方便解析lambda函数。
练完手之后,我们试着使用lambda表达式作为sort排序规则的参数。其中lambda表达式可以直接写在sort参数列表中。像这种返回值类型比较明确的,lambda中的返回值省不省略都可以。
int main()
{
vector<Student> v = { {"张三", 22, 84.9}, {"李四", 20, 88.4}, {"王五", 23, 86.7} };
//年龄升序
sort(v.begin(), v.end(), [](const Student& s1, const Student& s2)->bool
{
return s1._age < s2._age;
});
PrintVector(v);
//年龄降序
sort(v.begin(), v.end(), [](const Student& s1, const Student& s2)->bool
{
return s1._age > s2._age;
});
PrintVector(v);
//成绩升序
sort(v.begin(), v.end(), [](const Student& s1, const Student& s2)
{
return s1._grade < s2._grade;
});
PrintVector(v);
//成绩升序
sort(v.begin(), v.end(), [](const Student& s1, const Student& s2)
{
return s1._grade > s2._grade;
});
PrintVector(v);
return 0;
}
运行结果如下:
1.3 捕获列表 mutable关键字
我们使用lambda写一个交换函数。如下面代码所示,可以写一个引用进行交换。但是lambda函数不仅可以使用传递的参数,也可以使用捕获列表中的对象。
swap2对象中,捕获两个整型变量m和n。如果你不加mutable关键字,无法改变捕获的参数。因为捕获的参数使用了const进行修饰。mutable有可变的意思,加上之后就可以修改了,需要注意此时不能省略参数列表。
int main()
{
int m = 5, n = 10;
auto swap1 = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
swap1(m, n);
cout << "m->" << m << " " << "n->" << n << endl;
auto swap2 = [m, n]()mutable
{
int tmp = m;
m = n;
n = tmp;
};
swap2();
cout << "m->" << m << " " << "n->" << n << endl;
return 0;
}
运行结果如下,你会发现调用swap2之后,m和n的值没有变回初始化的值。这是为什么呢?
因为这种捕捉是传值捕捉,传值捕捉本质是外面参数的一份拷贝,并且使用const修饰。使用mutable仅仅是去掉了const属性,可以修改这份拷贝,对于外面的参数没有影响。
如果想不传递参数交换两个数,可以使用传引用捕捉。用法是在捕获列表参数前加上&符号,捕获列表的参数就是外面参数的别名。不用加上mutable,因为引用捕捉默认不用const修饰。
int main()
{
int m = 5, n = 10;
auto swap1 = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
swap1(m, n);
cout << "m->" << m << " " << "n->" << n << endl;
auto swap2 = [&m, &n]()
{
int tmp = m;
m = n;
n = tmp;
};
swap2();
cout << "m->" << m << " " << "n->" << n << endl;
return 0;
}
运行结果如下:
如果局部域中的变量很多,写lambda表达式中一个个写到捕获列表中很麻烦。此时,就可以使用=和&符号,前者表示所有值传值捕捉,后者表示所有值传引用捕捉,在成员函数中包含this指针。
最后还可以混合捕捉,如下面代码中,a使用传引用捕捉,b使用传值捕捉,需要用逗号隔开。还可以所有值传引用捕捉,一个变量传值捕捉,或者是所有值传值捕捉,一个变量传引用捕捉。
int main()
{
int a = 1, b = 2, c = 3, d = 4;
// 所有值传值捕捉
auto func1 = [=]
{
int ret = a + b + c + d;
return ret;
};
cout << func1() << endl;
cout << "a->" << a << " " << "b->" << b << " ";
cout << "c->" << c << " " << "d->" << d << endl;
// 所有值传引用捕捉
auto func2 = [&]
{
a += 2;
b--;
c++;
d /= 2;
int ret = a + b + c + d;
return ret;
};
cout << func2() << endl;
cout << "a->" << a << " " << "b->" << b << " ";
cout << "c->" << c << " " << "d->" << d << endl;
//混合捕捉
auto func3 = [&a , b]
{
a++;
int ret = a + b;
return ret;
};
cout << func3() << endl;
cout << "a->" << a << " " << "b->" << b << " ";
cout << "c->" << c << " " << "d->" << d << endl;
auto func4 = [&, c]
{
a += 2;
b--;
d /= 2;
int ret = a + b + c + d;
return ret;
};
cout << func3() << endl;
auto func5 = [=, &d]
{
d++;
int ret = a + b + c + d;
return ret;
};
cout << func3() << endl;
return 0;
}
运行结果如下:
1.4 lambda表达式原理
下面代码中,先定义了名为Rate的类,重载了()函数,使用Rate对象r1,使用类似函数的操作,完成对利率的计算,实际上调用的就是()重载函数。第二块代码使用的是lambda表达式,捕捉rate变量,完成利率的计算。
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.03;
// 函数对象
Rate r1(rate);
cout<< r1(10000, 2) <<endl;
// lamber
auto r2 = [rate](double money, int year)->double
{
return money * rate * year;
};
cout << r2(10000, 2) << endl;
return 0;
}
运行结果如下,它们的操作看起来十分相似,得到的结果也是相同的。
我们调试该代码,进入汇编代码中。如下图,我们知道r1在初始化的时候调用了Rate的构造函数,然后使用类似函数的调用操作,实际上是调用了operator()函数。
- 我们再来看lambda表达式的汇编代码,在用lambda表达式初始化r2时,会发现调用了<lambda_1>类的构造函数进行初始化,并且<lambda_1>在嵌套在许多类域之下。使用r2做类似函数调用的操作,在汇编代码发现也调用了<lambda_1>类的operator()函数。
- 说明lambda表达式写出的匿名函数是相对于我们而言的,编译器会在创建一个类,不同编译器对类取名规则不相同。并且每个lambda的生成的类名不同。
- 初始化时,会从捕获列表中获取参数作为该类的成员变量,并定义operator()函数,方便做出类似函数调用的操作。
2. 包装器
2.1 function
function包装器,也是一种适配器。C++中,function本质是一个类模版,可以包装任意类型的可调用对象。可调用对象有函数指针、仿函数(即定义了operator()函数的类)和lambda表达式。
其中传递function类模版参数时,要类似于函数返回值和参数列表的形式,只留下返回值类型+(参数类型)。注意使用function需要包<functiona>l头文件
#include<functional>
int fun(int x, int y)
{
return x + y;
}
struct Functor
{
int operator()(int x, int y)
{
return x + y;
}
};
int main()
{
//函数指针
function<int(int, int)> f1 = fun;
//仿函数类对象
function<int(int, int)> f2 = Functor();
//lambda表达式
function<int(int, int)> f1 = [](int x, int y) {return x + y; };
cout << f1(3, 4) << endl;
cout << f2(3, 4) << endl;
cout << f3(3, 4) << endl;
return 0;
}
运行结果如下:
包装上述对象正常操作即可,但是包装自定义类中的成员函数,有很多地方需要注意。
- 包装静态成员函数时,因为受到类域的限制,需要手动指定类域。
- 包装普通成员函数时,也要指定类域,并且函数名前要加上取地址符号,这是语法规定。普通成员函数的参数列表隐藏了this指针参数,需要匹配参数列表中的类型。有两种方法解决这个问题。
- 第一种方法是function包装时,写上自定义类指针类型。在调用时,创建该类对象,传入对象的地址。
- 第二种方法是在function传类参数上写上自定义类名。在调用时 ,可以直接传该类对象,或者是匿名对象。
为什么可以这样做呢?因为调用function类对象时,不是将参数直接传给成员函数,再实现函数调用。而是function拿到该函数的位置,并将其存储到function的成员变量中,在调用函数时,先调function中的operator(),再调该函数。所以不管是类对象指针,还是类对象,都可以调用到成员函数。
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
//静态成员函数,类中成员函数都要指定类域
function<int(int, int)> f4 = Plus::plusi;
//普通成员函数,需要加上取地址符号&
function<int(Plus*, int, int)> f5 = &Plus::plusd;
Plus pd;
f5(&pd, 1.1, 1.1);
//底层使用这个指针或者对象调用该函数
function<int(Plus, int, int)> f6 = &Plus::plusd;
f6(pd, 1.1, 1.1);
f6(Plus(), 1.1, 1.1);
return 0;
}
有function之后,可以建立一种变量跟包装器对象的映射关系。比如说一个数组中存储这一些字符串,里面有加减乘除的符号。如果不使用map容器建立字符串和函数的映射关系,那就要写switch判断或者是“if else“”语句,会十分麻烦。
int main()
{
map<string, function<int(int, int)>> operatorMap =
{
{"+", [](int x, int y) {return x + y; }},
{"-", [](int x, int y) {return x - y; }},
{"*", [](int x, int y) {return x * y; }},
{"/", [](int x, int y) {return x / y; }}
};
int a = 10, b = 4;
vector<string> tokens = { "+", "$", "/", "&", "*" };
for (auto& str : tokens)
{
if (operatorMap.count(str))
{
operatorMap[str](a, b);
}
}
return 0;
}
2.2 bind
bind函数也是定义在functional头文件中,是一个模版函数。它是一个函数包装器,参数列表如上,先接受一个可调用对象,生成一个新的可调用对象。bind可以调整可调用对象参数的个数和参数的顺序。如果要使用bind,还得了解placeholders命名空间。
placeholder有占位符的意思,该命名空间中有许多的占位符,_1可以替代传入的可调用函数的第一个参数,_2可以替代传入的可调用函数的第二个参数,_N可以替代传入的可调用函数的第N个参数。
先定义一个三个参数的Func函数,做简单的运算。可以先用using声明前几个占位符,使用占位符时就不用展开类域了。下面是调整参数顺序和个数的代码。
- 调整参数顺序,只需要交换占位符。比如交换Func函数中a和b参数传入的位置,只需要将bind中的_1和_2交换一下。
- 调整参数个数一般是指固定某个参数。假如要固定Func第二个参数b,需要将bind中原先放第二参数的位置写上整型值,其他位置要写_1和_2,表示剩下的实参。
int Func(int a, int b, int c)
{
int ret = (a - b) * c;
return ret;
}
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int main()
{
//调整参数顺序-->不常用
//按照Func参数顺序写,f1与Func调用情况一致
auto f1 = bind(Func, _1, _2, _3);
cout << f1(2, 1, 10) << endl;
//改变Func函数中第一个参数和第二个参数
auto f2 = bind(Func, _2, _1, _3);
cout << f2(2, 1, 10) << endl;
//调整参数个数-->常用
//固定函数中第二个参数
auto f3 = bind(Func, _1, 5, _2);
cout << f3(10, 10) << endl;
//固定函数中第一个参数
auto f4 = bind(Func, 10,_1, _2);
cout << f4(10, 10) << endl;
return 0;
}
运行结果如下:
上面使用function包装普通成员函数时,第一个参数需要传类对象或者类对象指针,十分麻烦。可以使用bind绑定第一个参数,固定成匿名类对象,方便调用。
当然,绑定器可以绑定某个函数的许多参数,提供一个固定条件的接口,只需要传入一个参数即可使用。
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int main()
{
Plus pd;
function<double(Plus*, double, double)> f1 = &Plus::plusd;
cout << f1(&pd, 1.1, 1.1) << endl;
function<double(Plus, double, double)> f2 = &Plus::plusd;
cout << f2(Plus(), 2.2, 2.2) << endl;
cout << f2(pd, 2.2, 2.2) << endl;
//使用bind绑定第一个参数
function<double(double, double)> f3 = bind(&Plus::plusd, Plus(), _1, _2);
cout << f3(3.3, 3.3) << endl;
return 0;
}
运行结果如下
创作不易,希望这篇文章能给你带来启发和帮助,如果喜欢这篇文章,请留下你的三连,你的支持的我最大的动力!!!