目录
一,lambda匿名函数
1-1,lambda的引入
1-2,lambda表达式书写格式
1-3,lambda函数的名称
1-4,lambda捕获列表的使用
1-5,函数对象与lambda表达式
二,包装器
2-1,function包装器
2-2,bind包装器
一,lambda匿名函数
1-1,lambda的引入
在C++中,lambda
函数是一种简洁的匿名函数或表达式,能够轻松处理复杂的逻辑和数据操作。lambda匿名函数可替换复杂的函数指针或伪函数,可以解决复杂而繁琐的仿函数和函数指针的使用,以及让程序员能够将类似于函数的表达式用作接收函数指针或伪函数的函数的参数(这点与function的作用有关)。
1-2,lambda表达式书写格式
lambda匿名函数:[capture-list] (parameters) mutable -> return-type { statement }
[capture-list]:捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量(捕获变量的值或引用‘&’)供lambda函数的函数体使用。
(parameters):参数列表,与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。lambda常性限制的是捕捉列表中的参数,没有限制参数列表中的参数。
->returntype:lambda函数的返回值类型(注意:不是表达式返回类型,下面会详细解释这方面),没有返回值时此部分可省略。由于lambda返回值类型相当于使用decltype根据返回值推断得到,如果lambda不包含返回语句,推断出的返回类型将为void,因此返回类型除非必要,一般不需要写。若使用该指定返回类型,参数列表将不可省略(即使参数为空)。
{statement}:lambda函数的函数体,与普通函数的函数体一样,包含实现功能的代码。
注意:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为 空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
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 } };
//下面两个排序需要编写两个访函数
sort(v.begin(), v.end(), ComparePriceLess()); //按价格从小到大排序
sort(v.begin(), v.end(), ComparePriceGreater()); //按价格从大到小排序
//下面两个排序使用lambda匿名函数,直接一个式子解决一种函数表达,可看出要比伪函数或函数指针(即直接函数实现)更简便
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; });
/*上面[](const Goods& g1, const Goods& g2) {return g1._price < g2._price; }会自动推导函数提返回类型是bool,即与[](const Goods& g1, const Goods& g2)->bool {return g1._price < g2._price; }等效*/
return 0;
}
1-3,lambda函数的名称
上面的lambda使用是作为单独函数表达式,下面将lambda表达式用作名称来使用(将该匿名函数赋给一个变量,该变量就是此名称)。lambda函数表达式必须要使用auto类型来接收。lambda底层实际上是一个类的仿函数,它在C++中并不直接返回它的结果,而是创建了一个可调用的对象(一个函数对象)。因此,你不能直接将一个lambda
表达式赋值给一个非函数对象类型的变量(如int
)。编译器会报错,因为int
类型期望一个int
类型或与int
型相似类型的值,但是得到了一个lambda
表达式(一个类类型)。不仅如此,编译器只有在编译时才可以确定lambda生成的式子,不同编译器下生成的式子还可能还不同,因此只能用auto接收lambda表达式,这里使用typeid().name可看出。
测试一:lambda的调用原理
//调用错误,lambda函数的返回值是int,但本身返回值不是int,是一个类类型
int a = [](int x)->int {return 55 + 5l; };
//auto自动推演类型,调用正确
auto _a = [](int x)->int {return 55 + 5l; };
//赋予lambda函数表达式名称为_a,而lambda底层是类的访函数,按照仿函数的调用即可
int b = _a(5);//上面lambda的调用相当于下面的类的仿函数调用
class A
{
public:
int operator()(int x)
{
return 55 + 51;
}
};A _a;
int b = _a(5);[](int x)->int {return 55 + 5l; };的调用相当于int x;A()(x);的调用
测试二:lambda原理调用的示例
#include <iostream>
using namespace std;int main()
{
int x = 10;
//使用lambda函数捕获x的引用并修改它
auto m = [&x]() {x = x * x; }; //m是一个实现访函数的类
m(); //lambda函数的调用
cout << "x: " << x << endl;
cout << typeid(m).name() << endl;
return 0;
}
lambda表达式是一个匿名函数,也可理解为一个表达式,该函数无法直接调用,如果想要直接调用,通常需要借助auto将其赋值给一个变量,通过该名称变量调用。
1-4,lambda捕获列表的使用
捕获列表说明:捕捉列表用于传入上下文中的数据(传值或引用),以便供lambda使用。这里捕获数据的方式有以下几种:
[var]:表示值传递方式捕捉变量var
[&var]:表示引用传递捕捉变量var
[=]:表示值传递方式捕获当前作用域中所有的变量(包括this)
[&]:表示引用传递捕获当前作用域中所有的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
#include <iostream>
using namespace std;
int main()
{
int a = 10, b = 10;
//值传递捕捉a,捕捉的是a的拷贝
auto fun1 = [a, b]()mutable { a += 1; b += 1; return a + b; }; /*lambda默认是const函数,限制了捕捉列表的参数,这里要使用mutable取消对捕捉列表的限制*/
cout << "fun1: a + b = " << fun1() << endl;
cout << "a + b = " << a + b << endl;
cout << endl;
//引用传递捕捉b,捕捉的是b的引用
auto fun2 = [&a, &b] {a += 1; b += 1; return a + b; }; /*捕捉列表引用捕捉,说明开发者想要对其修改,lambda没有对捕捉列表限制,可以修改*/
cout << "fun2: a + b = " << fun2() << endl;
cout << "a + b = " << a + b << endl;
cout << endl;
//值传递获取当前作用域所有变量,捕捉所有变量的拷贝
int c = 10, d = 10;
auto fun3 = [=]()mutable {c += 1; d += 1; return c + d; }; //与上面“值传递捕捉a”同理
cout << "fun3: c + d = " << fun3() << endl;
cout << "c + d = " << c + d << endl;
cout << endl;
//引用传递获取当前作用域所有变量,捕捉所有变量的引用
auto fun4 = [&] {c += 1; d += 1; return c + d; }; //与上面“引用传递捕捉b”同理
cout << "fun4: c + d = " << fun4() << endl;
cout << "c + d = " << c + d << endl;
cout << endl;
//捕捉a的拷贝,其它数据的引用
auto fun5 = [&, a]()mutable {a = 1; b = 1; c = 1; return a + b + c; };
/*auto fun6 = [=, &a]()mutable {a = 1; b = 1; c = 1; return a + b + c; };这里也可捕捉a的引用,其它数据的拷贝*/
cout << "fun5: a + b + c = " << fun5() << endl;
cout << "a + b + c = " << a + b + c;
cout << endl;
return 0;
}
这块需说明以下几个注意点:
1,语法上捕捉列表可由多个捕捉项组成,并以逗号分割,上面最后一个例子运用的就是此原理。还有比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量。
2,捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复。
4,在作用域中的lambda函数仅能捕捉当前作用域中局部变量,捕捉任何非此作用域或者
非局部变量都会导致编译报错。
5,lambda表达式之间不能相互赋值,即使看起来类型相同,因为无论怎样lambda的类型都不可能一样,它的底层是类的仿函数,编译时确定的类型会有所不同。
6,lambda底层实现了拷贝构造,但是禁掉了默认构造。
#include <iostream>
using namespace std;
void (*PF)(); //函数指针的声明
int main()
{
auto a = []{cout << "Hello C++" << endl; };
auto b = []{cout << "Hello C++" << endl; };/*a = b; 编译失败,lambda实现虽都一样,但两者的类型不一样,内部不存在不同类型间的赋值。从下面的输出可看出*/
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << endl;
/*允许使用一个lambda表达式拷贝构造一个新的副本,两者的类型相同,但lambda对象不能默认实现构造*///decltype(a) c; 默认构造的调用,编译失败
auto c(a);
cout << typeid(c).name() << endl;
cout << typeid(a).name() << endl;
cout << endl;
c();
//也可以将lambda表达式赋值给相同类型的函数指针
PF = a;
PF();
return 0;
}
1-5,函数对象与lambda表达式
C++的函数对象通常指的是仿函数。lambda与函数对象极为相似,实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator(),即仿函数。下面的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.49;
Rate r1(rate);
r1(10000, 2);
//lamber表达式
auto r2 = [=](double monty, int year)->double {return monty * rate * year; };
r2(10000, 2);
return 0;
}
二,包装器
2-1,function包装器
function包装器也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。由于函数指针较为复杂,仿函数比较单一,只能实现一种功能,如上Goods的比较,lambda函数语法层中没有类型,auto接收的只是名称,它们各有特色,但比较零散。function包装器的主要作用是封装函数指针(包括普通函数)、函数对象(仿函数)、lambda函数(匿名函数),将它们同一类型,实现一个函数的调用。我们先观察下面代码。
代码一:
#include <iostream>
using namespace std;
template<class F, class T>
T useF(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 << useF(f, 11.11) << endl;
cout << endl;
//函数对象(仿函数)
cout << useF(Functor(), 11.11) << endl;
cout << endl;
//lamber表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
cout << endl;
return 0;
}
代码二:
#include <iostream>
using namespace std;
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
int f1(double i) { return i / 2; }
double f2(double i) { return i / 3; }
double f3(double i) { return i / 4; }
double f4(int i) { return i / 5; }
int main()
{
/*实例化出的函数返回类型、参数类型相同,输出的count相同,即同一个函数,但若是其中一个类型不同,将会实例化出不同的函数,即输出count不同*/
cout << useF(f1, 11.11) << endl;
cout << useF(f2, 11.11) << endl;
cout << useF(f3, 11.11) << endl;
cout << useF(f4, 11.11) << endl;
return 0;
}
通过上面的程序验证,我们会发现对于函数模板而言,当返回类型和参数类型一致的情况下,编译器不会重新实例化出一份新的函数,但对于函数指针、函数对象、lambda表达式而言,即便三者的函数返回类型、形参类型都相同,但useF函数模板还是实例化了三份。包装器function可以很好的解决上面的问题,将它们封装成一种函数。
std::function在头文件<functional>
类模板结构原型如下
template <class T> function;
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret : 被调用函数的返回类型
Args…:被调用函数的形参类型
使用方法如下:
#include <iostream>
#include<functional>
using namespace std;template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}double f1(double i) { return i / 2; }
int f2(double i) { return i / 2; }
double f3(int i) { return i / 3; }struct Functor
{
double operator()(double d) { return d / 3; }
};int main()
{
//函数指针
function<double(double)> fc1 = f1;
fc1(11.11); //将f1封装成fc1,返回类型double、形参类型double
cout << useF(fc1, 11.11) << endl;
/*下面的函数指针形式输出发现与上面的不同,返回类型和参数类型只要有一个不同,函数模板实例化出的函数就不同*/
function<double(int)> fcc1 = f2;
fcc1(11.11); //将f2封装成fc,此时返回类型int、形参类型double,与f2不同
cout << useF(fcc1, 11.11) << endl;
//封装函数返回类型和形参类型相同,调用函数相同
function<double(double)> fcc2 = f3;
fcc2(11.11); //将f3封装成fc2,此时返回类型double、形参类型double,与f3不同
cout << useF(fcc2, 11.11) << endl;
cout << endl;//函数对象
function<double(double)> fc2 = Functor();
fc2(11.11);
cout << useF(fc2, 11.11) << endl;//lambda表达式
function<double(double)> fc3 = [](double d)->double { return d / 4; };
fc3(11.11);
cout << useF(fc3, 11.11) << endl;return 0;
}
function包装器封装时的函数返回类型与形参类型可以与原函数不同,当包装器包装后,此时的调用情况与上面代码二一样,即若函数返回类型和形参类型相同将不会再新实例化出一份函数。
function包装器包装后的调用与原函数互不影响,如上fc1与fc两者调用的函数不同,这里可放心使用。
类的成员函数的包装
类的成员函数分为静态成员函数和非静态成员函数。静态成员函数没有包含隐藏的this指针,函数名即为函数地址,包装时跟其它函数包装一样,没有任何变化。非静态成员函数由于第一个参数是隐藏的this指针,所以语法规定包装时第一个形参必须是对象的指针或对象,其次,类的成员函数名本身不是地址,所以这里传递时必须传递地址,即“&”。
#include <iostream>
#include<functional>
using namespace std;
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};int f(int a, int b) { return a + b; }
int main()
{
//普通函数
function<int(int, int)> fc1 = f;
function<int(int, int)> f1 = &f;
cout << fc1(1, 1) << " " << f1(1, 1) << endl;
cout << f << " " << &f << endl; //输出地址一样,加不加&都行//静态成员函数
function<int(int, int)> fc2 = &Plus::plusi;
function<int(int, int)> fc = Plus::plusi;
cout << fc2(1, 1) << " " << fc(1, 1) << endl;
cout << &Plus::plusi << " " << Plus::plusi << endl; //输出地址一样,加不加&都行//非静态成员函数
//非静态成员函数需要对象的指针或者对象去进行调用,因为类的非静态成员函数第一个默认隐形的参数是this指针
Plus plus;//对象指针。这里不能使用Plus::plusd,因为成员函数名不是地址
function<double(Plus*, double, double)> fc3 = &Plus::plusd;
cout << fc3(&plus, 1, 1) << endl;//对象。这里不能使用Plus::plusd
function<double(Plus, double, double)> fc4 = &Plus::plusd;
cout << fc4(plus, 1, 1) << " " << fc4(Plus(), 1, 1) << endl;
return 0;
}
2-2,bind包装器
bind是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。它与function底层其实都是仿函数,返回的其实是一个可调用对象。
bind的作用有两个:
1,调整可调用对象参数的顺序(通常意义不大,了解即可)。
2,调整可调用对象参数的个数(具有一定的价值)。
bind原型结构如下
形式一:
template <class Fn, class... Args>
bind(Fn&& fn, Args&&... args);
形式二:(参数全部指定不能使用function接收,具体下面会说明)
template <class Ret, class Fn, class... Args>
bind(Fn&& fn, Args&&... args);
bind调用的一般形式:
1,auto万能接收
auto newCallable = bind(callable, arg_list);
2,function接收
function<Ret(Args...)> newCallable = bind(callable, arg_list);
newCallable:是一个可调用对象,可以是函数、成员函数、函数对象或lambda表达式。
arg_list:是一个传递给callable,并以逗号分隔的形参列表。若是直接给定 callable 的参数,当我们调用newCallable时,newCallable会调用callable,并传给指定的参数;若是 arg_list 中的参数存在占位符(placeholders占位符),则可以在稍后调用newCallable时提供这些参数。
占位符placeholders表示参数的“占位”。占位符_1、_2等表示绑定对象被调用时应提供的参数。
#include <iostream>
#include<functional>
using namespace std;int Sub(int a, int b)
{
return a - b;
}class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}double plusd(double a, double b)
{
return a - b;
}
};int main()
{
//普通函数的正常参数顺序使用
int x = 10, y = 20;
auto f1 = bind(Sub, 10, 20);
cout << f1() << endl; //指定参数,不需要传递auto f2 = bind(Sub, placeholders::_1, placeholders::_2); //成员_1表示传递Sub第一个位置参数,_2表示传递第二个位置参数
cout << f2(x, y) << endl; //占位符使用,x传递绑定对象f2的第一个参数,y传递第二个/*调整参数顺序,f2第一个参数接收Sub的第二个位置上的参数,第二个参数接受Sub的第一个位置上的参数(此运用了解一下,意义不大)*/
function<int(int, int)> f3 = bind(Sub, placeholders::_2, placeholders::_1);
cout << f3(x, y) << endl;//类的非静态成员函数绑定
Plus p;
/*bind绑定Plus::plusd,此处要指名地址(成员函数名称不是地址),具体绑定到p对象上,后面是绑定参数*/
function<double(double, double)> fc4 = bind(&Plus::plusd, p, placeholders::_1, placeholders::_2);
cout << fc4(2, 3) << endl;function<double(double)> fc5 = bind(&Plus::plusd, Plus(), placeholders::_1, 20); //绑定匿名对象Plus上
cout << fc5(2) << endl;//类的静态成员函数绑定
auto fc6 = bind(Plus::plusi, 10, 20); //静态成员不属于任何类,无需指名具体对象
//function<int(int, int)>fc6 = bind(Plus::plusi, 5, 7);参数全部指定,此时不能使用function
cout << fc6() << endl;
return 0;
}