目录
lambda表达式
Lambda表达式是一种在被调用的位置或作为参数传递给函数的位置定义匿名函数对象的简便方法。如下情景我们要对一些数进行排序,就可以调用sort函数,根据要求不同传递参数不同。如下代码。
struct SortDown
{
bool operator()(const int& x,const int &y )
{
return x > y;
}
};
int main()
{
vector<int> a = { 1,5,2,62,61,2,22,36,6,10 };
//默认升序
sort(a.begin(), a.end());
for (int e : a)
cout << e << " ";
cout << endl;
//降序
sort(a.begin(), a.end(), SortDown());
for (int e : a)
cout << e << " ";
cout << endl;
return 0;
}
运行结果如下图
上述代码我们也可以实现对不同要求的排序,但每次每次实现要求时要单独写一个类,相较而言是比较复杂的,就可以用lambda简化如下代码。
int main()
{
vector<int> a = { 1,5,2,62,61,2,22,36,6,10 };
//默认升序
sort(a.begin(), a.end(), [](const int& x, const int& y)->bool {return x < y; });
for (int e : a)
cout << e << " ";
cout << endl;
//降序
sort(a.begin(), a.end(), [](const int& x, const int& y)->bool{return x > y; });
for (int e : a)
cout << e << " ";
cout << endl;
return 0;
}
运行结果如下。
相较于使用类,使用lambda表达式更加的简洁些,可以直接看出sort的排序不用向上查找类实现。
lambda语法
Lambda表达式可以看为是匿名函数对象,语法如下。
[capture-list] (parameters) mutable -> return-type { statement }
首先 return-type是返回值类型, statement是函数体,parameters是函数参数,这三个与普通函数基本一样,按照普通函数用法去写即可。
其中有些特殊规定如下。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以 连同()一起省略。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回 值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推 导。
以上两处在特定的情况下都可以省略,所以可以说lambda表达式基本结构如下。
[] { }
捕获列表加上函数体。
capture-list捕获列表
1.[var]:表示值传递方式捕捉变量var
按照值传递方式捕捉变量var,相当于拷贝一份var变量,在函数体内是不支持修改的,具有const属性,发生修改操作就会报错。如下图。
如果想要var变量可以修改就要加上mutable关键字。如下图就可以修改了。
接下来通过调试观察a,b值变化。
由于lambda表达式类型是匿名的,我们就可以用auto接受,让编译器自己推导。如何使用这个匿名函数对象,我们就可以直接调用()就可以了,把他视为一个对象,在对象中将()进行了重载。
如下代码。
int main()
{
int a = 0, b = 2;
auto fun=[a, b]()mutable{a = 1;b = 1;};
fun();
return 0;
}
单纯只写函数名是不可以修改外部变量的!!
2.[=]:表示值传递方式捕获所有父作用域中的变量
如果在捕获列表写一个=,说明所有要使用的外部变量都按照值传递的方式,在函数体内部修改不影响外面。如下代码。
int main()
{
int a = 0, b = 2;
cout << a <<" " << b << endl;
auto fun=[=]()mutable{
a = 1;
b = 1;
};
fun();
cout << a << " " << b << endl;
return 0;
}
运行结果如下,在函数内修改不影响外面。
如果我们想要影响函数外面就可以传引用的方式。
3. [&var]:表示引用传递捕捉变量var
此时变量var就以引用的方式传递进入lambda表达式,如上代码修改后结果如下。
int main()
{
int a = 0, b = 2;
cout << a <<" " << b << endl;
auto fun=[&a,&b]()mutable{
a = 1;
b = 1;
};
fun();
cout << a << " " << b << endl;
return 0;
}
这里的&a表示按引用传递为不是取地址,是对&运算符的再次利用赋予新的含义。
4.[&]:表示引用传递捕捉所有父作用域中的变量
捕获列表如果只写一个&,表示所有使用的变量都以引用的方式传递。
int main()
{
int a = 0, b = 2;
cout << a <<" " << b << endl;
auto fun=[&]()mutable{
a = 1;
b = 1;
};
fun();
cout << a << " " << b << endl;
return 0;
}
引用之后修改的就与外层变量是同一个。
5.混合使用
除了上述使用外,我们可能会有多个变量要引用,一个要值传递就可以采用如下方式。
int main()
{
int a = 0, b = 2;
int c = 3;
cout << a <<" " << b <<" " << c << endl;
auto fun=[&,c]()mutable{
a = 1;
b = 1;
c = 1;
};
fun();
cout << a << " " << b << " " << c << endl;
return 0;
}
c按照值传递,a,b按照引用传递。这样就可以避免写多个引用或者值传递。
同理也可以按照如下使用,=加引用传递
int main()
{
int a = 0, b = 2;
int c = 3;
cout << a <<" " << b <<" " << c << endl;
auto fun=[=,&c]()mutable{
a = 1;
b = 1;
c = 1;
};
fun();
cout << a << " " << b << " " << c << endl;
return 0;
}
6.注意事项
1.不可以重复捕获一个变量。
c表示值传递,=又把变量c值传递,编译器就会报错。但可以将c改为&c,按引用传递,那么就不会再对c进行值传递了。
2.lambda表达式只可以捕获当前父作用域变量。
父作用域指包含lambda函数的语句块。通常被{}包围。
3.lambda表达式内可以使用全局变量。
如下代码
int c = 3;
void test()
{
int a = 0, b = 2;
cout << a << " " << b << " " << c << endl;
auto fun = [=]()mutable {
a = 1;
b = 1;
c = 1;
};
fun();
cout << a << " " << b << " " << c << endl;
}
int main()
{
test();
return 0;
}
运行结果如下。
lambda与仿函数(函数对象)
lambda表达式的结果就是匿名函数对象,我们无法直接的得到他的类型,只能通过编译器推导,即使用auto。
运行如下代码。我们可以得到如下结果。
struct SortDown
{
bool operator()(const int& x,const int &y )
{
return x > y;
}
};
void test()
{
vector<int> a = { 1,5,2,62,61,2,22,36,6,10 };
auto fun = [](const int& x, const int& y) {
return x > y;
};
//lambda表达式
sort(a.begin(), a.end(), fun);
for (int e : a)
cout << e << " ";
cout << endl;
//仿函数
sort(a.begin(), a.end(), SortDown());
for (int e : a)
cout << e << " ";
cout << endl;
}
int main()
{
test();
return 0;
}
我们可以通过仿函数得到与lambda一样的效果。
当然也可以让他们两形式更像一些。此时都是匿名对象。
void test()
{
vector<int> a = { 1,5,2,62,61,2,22,36,6,10 };
//lambda表达式
sort(a.begin(), a.end(), [](const int& x, const int& y) {return x > y;});
for (int e : a)
cout << e << " ";
cout << endl;
//仿函数
sort(a.begin(), a.end(), SortDown());
for (int e : a)
cout << e << " ";
cout << endl;
}
我们可以看些底层汇编加深他们之间的关系。运行如下代码
struct SortDown
{
bool operator()(const int& x,const int &y )
{
return x > y;
}
};
int main()
{
auto fun = [](const int& x, const int& y) {
return x > y;
};
//lambda表达式
fun(1, 2);
SortDown sd;
//仿函数
sd(1, 2);
return 0;
}
因此可以说lambda表达式就是经过特殊处理的函数对象,对象名在运行时由编译器决定,可以使用auto推出,lambda表达式使用与仿函数一样,都是调用重载的()函数。
lambda优点
相较于类而言,lambda表达式更加的简洁,明了。可以在可以在传递参数的位置让程序员明白意图,不必向上寻找类,总而言之面对较简单的逻辑时,就可以考虑用lambda表达式替换类对象简化代码。
包装器
为什么要有包装器(适配器)
在使用包装器之前,我们要包含头文件<functional>。
通过上述lambda表达式的学习,我们知道一个名字加()可以形成具有函数作用的语句有三种。如下代码
1.函数名,这也是我们最早学的,用的最多的。
int cmp(const int& x, const int& y)
{
return x < y;
}
//调用语句
cout<<cmp(1, 2);
2.类重载()
struct up
{
bool operator()(const int& x, const int& y)
{
return x < y;
}
};
up t;
//调用语句
cout<<t(1, 2);
3.lambda表达式
auto fun = [](const int& x, const int& y) {
return x < y;
};
//调用语句
cout<<fun(1, 2);
假如把上述3种的调用写在一起如下。
//调用语句
cout<<cmp(1, 2);
cout<<t(1, 2);
cout<<fun(1, 2);
光看名称很难做出判断,这个名字究竟是类名?函数名?此时我们在设计深一层的接口时就十分难办,如排序接口,第三个参数写出什么?指针么?传对象不可以。对象么?指针又不可以。在标准库中sort第三个参数就是开放的,可以传类或者指针。如下图。
于是就引入了适配器的概念,不关心你是通过函数还是对象实现的,只关注你要什么参数,返回值是什么,统一模板。如下代码。
int main()
{
up t;
function<bool(int, int)> f1 = cmp;
function<bool(int, int)> f2 = t;
function<bool(int, int)> f3 = fun;
//调用语句
cout<<f1(1, 2)<<endl;
cout<<f2(1, 2)<<endl;
cout<<f3(1, 2)<<endl;
return 0;
}
这里的类型不要求完全相同,可以走隐式类型转化,当然完全相同最好。
经过上述适配器后,我们就不需要关注是怎么是实现的,只关注如何使用,运行结果如下。
function
function实际上是一个模板类,可以根据提供的参数,实例化不同的类。function - C++ Reference (cplusplus.com)
底层构造函数根据类型不同进行不同的初始化,这里就不过多赘述了,感兴趣可以点击上面网站查询。
他的语法如下
function<返回值类型(参数列表)> 变量名 = 函数名/类名/lambda表达式;
例子:function<bool(int, int)> f1 = cmp;
其中比较特殊的情况是function接受类函数。
1.普通函数
普通函数接受只要按照语法写好返回值与参数即可,函数名可以加上&也可以不加。
int cmp(const int& x, const int& y)
{
return x < y;
}
int main()
{
function<bool(int, int)> f1 = cmp;
//调用语句
cout<<f1(1, 2)<<endl;
return 0;
}
如下图,两者运行结果一样·。
2.类静态成员函数
如果是类静态成员函数可以肢解使用类域加函数名方式使用,&加不加都可以。如下代码。
struct st
{
static int cmp(int x, int y)
{
return x < y;
}
};
int main()
{
function<bool(int, int)> f1 = st::cmp;
//调用语句
cout<<f1(1, 2)<<endl;
return 0;
}
运行结果如下图。
3.类普通函数
如果为普通成员函数,那么函数名之前必须要加上&符号。但只有这还不行,运行下列程序会爆出如下图错误。
struct st
{
int cmp(int x, int y)
{
return x < y;
}
};
int main()
{
function<bool(int, int)> f1 = &st::cmp;
//调用语句
cout<<f1(1, 2)<<endl;
return 0;
}
这个实际上就是this指针,普通成员函数默认会传递this指针,所以要将函数参数修改才可以。
struct st
{
int cmp(int x, int y)
{
return x < y;
}
};
int main()
{
st tmp;
function<bool(st*,int, int)> f1 = &st::cmp;
//调用语句
cout<<f1(&tmp,1, 2)<<endl;
return 0;
}
修改后如下图运行结果。
在C++11中特殊规定了成员函数第一个参数可以用类指针,也可以用对象,如下代码也是可以正常运行的。与上述代码结果一样。
struct st
{
int cmp(int x, int y)
{
return x < y;
}
};
int main()
{
st tmp;
function<bool(st,int, int)> f1 = &st::cmp;
//调用语句
cout<<f1(tmp,1, 2)<<endl;
return 0;
}
4.function优化
通过function也可以解决类模板效率底下的问题。如下代码。
template<class F, class T>
T fun(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数名
cout << fun(f, 11.11) << endl;
// 函数对象
cout << fun(Functor(), 11.11) << endl;
// lamber表达式
cout << fun([](double d)->double { return d / 4; }, 11.11) << endl;
return 0;
}
程序在运行时应为参数不同,会实例化出三份模板,但如果将代码改为如下就可以提高效率
template<class F, class T>
T fun(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数名(函数指针)
std::function<double(double)> func1 = f;
cout << fun(func1, 11.11) << endl;
// 函数对象
std::function<double(double)> func2 = Functor();
cout << fun(func2, 11.11) << endl;
return 0;
}
fun1与fun2的类型一样,最终只会实例化一份代码,提高效率。
bind
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可 调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而 言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个参数的新函数。同时,使用std::bind函数还可以实现参数顺 序调整等操作。
模板如下,bind - C++ Reference (cplusplus.com)
如果为函数语法如下。返回值可以用auto接收,也可以用function接受。
bind(函数名,参数列表)
参数列表中_1 表示调用函数时传递的第一个参数(即第一个实参),_2表示调用函数时传递的第二个参数(即第二个实参),他们存在类域placeholders中。
其中参数列表可以掺入常量,表示那个参数绑定死某个值,具体用法看下述代码。
如果为类成员函数语法如下。返回值可以用auto接收,也可以用function接受。
bind(&函数名,类对象/指针,参数列表)
参数列表中_1 表示调用函数时传递的第一个参数,_2表示调用函数时传递的第二个参数等,他们存在类域placeholders中。
其中参数列表可以掺入常量,表示那个参数绑定死某个值,具体用法看下述代码。
1.改变参数位置
为了使用方便我们可以直接声明使用空间placeholders,即如下代码。
using namespace placeholders;
using namespace placeholders;
int sub(int x, int y)
{
return x - y;
}
int main()
{
auto fun = bind(sub, _1, _2);
cout << fun(5, 4) << endl;
cout << fun(6, 1) << endl;
cout << fun(4, 9) << endl;
return 0;
}
运行结果如下。
此时函数参数对应关系如下。
如果将绑定顺序修改,运行结果如下。
auto fun = bind(sub, _2, _1);
2.改变参数个数
例如上述代码就可以将某个参数修改为定值,绑死某个参数,此时占位参数只能从_1开始,不能从_2开始。
int sub(int x, int y)
{
return x - y;
}
int main()
{
auto fun1 = bind(sub, 10, _1);
auto fun2 = bind(sub, _1, 5);
cout << fun1(5) << endl;
cout << fun2(6) << endl;
return 0;
}
运行结果如下。
3.综合使用
运行如下代码
int Add(int a, int b)
{
return a + b;
}
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};
int main()
{
//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定
std::function<int(int, int)> func1 = std::bind(Add, placeholders::_1,placeholders::_2);
//Add前两个参数绑死1 2
auto func2 = std::bind(Add, 1, 2);
cout << func1(1, 2) << endl;
cout << func2() << endl;
Sub s;
// 绑定成员函数
std::function<int(int, int)> func3 = std::bind(&Sub::sub, s,
placeholders::_1, placeholders::_2);
// 参数调换顺序
std::function<int(int, int)> func4 = std::bind(&Sub::sub, s,
placeholders::_2, placeholders::_1);
cout << func3(1, 2) << endl;
cout << func4(1, 2) << endl;
return 0;
}
结果如下图。
func3的顺序没有改变如下图。
func4的顺序有改变如下图。
总之包装器进一步封装了代码,提供了简洁的使用方式。