文章目录
- 一、lambda表达式
- 1. 为什么需要lambda表达式
- 2. lambda的定义
- 3. lambda的语法
- 捕捉列表
- 4. 函数对象和lambda表达式的底层原理
- 二、函数包装器
- 1. function包装器
- 2. bind包装器
- 用bind包装器绑定固定参数
- 用bind包装器调整传参顺序
- 无意义的绑定
- 3. bind包装器的意义
一、lambda表达式
1. 为什么需要lambda表达式
在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort
方法。
#include <algorithm>
#include <functional>
int main()
{
int array[] = {4,1,8,5,3,7,0,9,2,6};
// 默认按照小于比较,排出来结果是升序
std::sort(array, array+sizeof(array)/sizeof(array[0]));
// 如果需要降序,需要改变元素的比较规则
std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
return 0;
}
如果待排序元素为自定义类型,需要用户使用仿函数定义排序时的比较规则:
struct Goods
{
string _name; //名字
double _price; //价格
int _num; //数量
};
struct ComparePriceLess
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._price < g2._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._price > g2._price;
}
};
struct CompareNumLess
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._num < g2._num;
}
};
struct CompareNumGreater
{
bool operator()(const Goods& g1, const Goods& g2)
{
return g1._num > g2._num;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 300 }, { "香蕉", 3.3, 100 }, { "橙子", 2.2, 1000 }, { "菠萝", 1.5, 1 } };
sort(v.begin(), v.end(), ComparePriceLess()); //按价格升序排序
sort(v.begin(), v.end(), ComparePriceGreater()); //按价格降序排序
sort(v.begin(), v.end(), CompareNumLess()); //按数量升序排序
sort(v.begin(), v.end(), CompareNumGreater()); //按数量降序排序
return 0;
}
仿函数是重载了operator()
的类,仿函数确实能够解决这里的问题,但可能仿函数的定义位置可能和使用仿函数的地方隔得比较远,这就要求仿函数的命名必须要通俗易懂,否则会降低代码的可读性。对于这种场景就比较适合使用lambda表达式。
2. lambda的定义
lambda表达式首先是一个可调用对象,是一个无名函数,一个lambda表达式可以被赋值给std::function
对象,或者直接使用auto类型推导。std::function
是一个通用的函数封装器,它可以包装任意可调用对象,包括函数指针、函数对象、成员函数指针以及lambda表达式。
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
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; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._evaluate < g2._evaluate; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._evaluate > g2._evaluate; });
}
lambda表达式提高了代码的可读性
3. lambda的语法
lambda表达式书写格式:
[capture-list] (parameters) mutable -> return_type { statement }
lambda表达式各部分说明:
[capture-list]
: 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据 [ ] 来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。(parameters)
:参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略mutable
:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。->return_type
:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。{statement}
:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda表达式为:
[]{};
,该lambda函数不能做任何事情。
捕捉列表
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用:
-
[var]:表示值传递方式捕捉变量var,默认情况下var在lambda作用域内是const,不可修改,除非lambda被mutable修饰。
-
[=]:表示值传递方式捕获所有父作用域中的变量(成员函数包括this指针)。
-
[&var]:表示引用传递捕捉变量var。
-
[&]:表示引用传递捕捉所有父作用域中的变量(成员函数包括this指针)。
-
[this]:表示值传递方式捕捉当前的this指针。
注意:
- 父作用域指包含lambda函数的语句块
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:
[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
[&, a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量- 捕捉列表不允许变量重复传递,否则就会导致编译错误。
比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复- 在块作用域以外的lambda函数捕捉列表必须为空。
- 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
- lambda表达式之间不能相互赋值,即使看起来类型相同
void (*PF)(); int main() { auto f1 = [] {cout << "hello world" << endl; }; auto f2 = [] {cout << "hello world" << endl; }; // 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了 f1 = f2; // 编译失败--->提示找不到operator=() // 允许使用一个lambda表达式拷贝构造一个新的副本 auto f3(f2); f3(); // 可以将lambda表达式赋值给相同类型的函数指针 PF = f2; PF(); return 0; }
4. 函数对象和lambda表达式的底层原理
函数对象,又称为仿函数,即可以像函数一样使用的对象,本质是在类中重载了operator()
运算符的类对象。
C++中哪些是可调用对象?
可调用对象(Callable Objects)是指可以像函数一样被调用的实体。在C++中,有多种类型的可调用对象,包括以下几种:
- 函数指针(Function Pointers):
- 普通函数指针,指向普通函数。
- 成员函数指针,指向类的成员函数。
// 普通函数指针 int (*functionPointer)(int, int); // 成员函数指针 class MyClass { public: int memberFunction(int, int); }; int (MyClass::*memberFunctionPointer)(int, int) = &MyClass::memberFunction;
- 函数对象(Function Objects):
- 也称为仿函数,是一个重载了
operator()
的类对象,可以像函数一样被调用。struct MyFunctor { int operator()(int a, int b) { return a + b; } }; MyFunctor myFunctor;
- Lambda 表达式:
- 匿名函数,可以在需要时直接定义和使用。
auto lambda = [](int a, int b) { return a + b; };
- std::function 对象:
- 是一个通用的函数封装器,可以包装任何可调用对象,包括函数指针、函数对象、成员函数指针以及 Lambda 表达式等。
#include<functional\> std::function<int(int, int)> myFunction = [](int a, int b) { return a + b; };
- 成员函数指针与对象绑定:
- 成员函数指针可以与特定的对象绑定,形成成员函数指针和对象的组合。
class MyClass { public: int memberFunction(int, int); }; MyClass myObject; int (MyClass::*boundMemberFunctionPointer)(int, int) = &MyClass::memberFunction;
下面的代码编写了一个Add类,也编写了一个类似的lambda表达式:
class Add
{
public:
Add(int base)
:_base(base)
{}
int operator()(int num)
{
return _base + num;
}
private:
int _base;
};
int main()
{
int base = 1;
//函数对象
Add add1(base);
//下面两种调用方法是一样的
add1.operator()(1000);
add1(1000);
//lambda表达式
auto add2 = [base](int num)->int
{
return base + num;
};
add2(1000);
return 0;
}
通过反汇编可以看到,实际编译器在底层对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的:
如何理解lambda表达式之间不能相互赋值?
lambda表达式之间不能相互赋值,就算是两个一模一样的lambda表达式。
- 因为lambda表达式底层的处理方式和仿函数是一样的,在VS2017下,lambda表达式在底层会被处理为函数对象,该函数对象对应的类名叫做<lambda_uuid>
- 因此每个lambda表达式都有自己的唯一类型。这是因为lambda表达式在编译时被翻译成一个匿名的函数对象类,每个lambda表达式都会生成一个不同的类类型。。
- 类名中的uuid叫做通用唯一识别码(Universally Unique Identifier),简单来说,uuid就是通过算法生成一串字符串,保证在当前程序当中每次生成的uuid都不会重复,当然VS2022处理的方式可能有所不同,但是目的是一样的:每个lambda底层对应的类名不同,保证每个lambda表达式底层类名都是唯一的。
因此每个lambda表达式的类型都是不同的,这也就是lambda表达式之间不能相互赋值的原因,我们可以通过typeid(变量名).name()
的方式来获取lambda表达式的类型。比如:int main() { int a = 10, b = 20; auto Swap1 = [](int& x, int& y)->void { int tmp = x; x = y; y = tmp; }; auto Swap2 = [](int& x, int& y)->void { int tmp = x; x = y; y = tmp; }; cout << typeid(Swap1).name() << endl; cout << typeid(Swap2).name() << endl; return 0; }
对于上面的代码,在VS2022下,就算是两个一模一样的lambda表达式,它们的类型都是不同的:
二、函数包装器
1. function包装器
function包装器
也叫作适配器。C++中的function
本质是一个类模板,也是一个包装器。
我们为什么需要function
呢?下面是一个模板被实例化多份的问题:
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 << useF(Functor(), 11.11) << endl;
// lambda表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
return 0;
}
通过上面的程序验证,我们会发现useF函数模板实例化了三份:
包装器可以很好的解决上面的问题
std::function在头文件<functional>中
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
- Ret: 被调用函数的返回类型
- Args…:被调用函数的形参
使用方法如下:
#include <functional>
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator() (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()
{
// 函数名(函数指针)
std::function<int(int, int)> func1 = f;
cout << func1(1, 2) << endl;
// 函数对象
std::function<int(int, int)> func2 = Functor();
cout << func2(1, 2) << endl;
// lambda表达式
std::function<int(int, int)> func3 = [](const int a, const int b)
{return a + b; };
cout << func3(1, 2) << endl;
// 类的成员函数
std::function<int(int, int)> func4 = &Plus::plusi;
cout << func4(1, 2) << endl;
std::function<double(Plus, double, double)> func5 = &Plus::plusd;
cout << func5(Plus(), 1.1, 2.2) << endl;
return 0;
}
有了包装器,如何解决模板的效率低下,实例化多份的问题呢?
#include <functional>
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()
{
// 函数名
std::function<double(double)> func1 = f;
cout << useF(func1, 11.11) << endl;
// 函数对象
std::function<double(double)> func2 = Functor();
cout << useF(func2, 11.11) << endl;
// lambda表达式
std::function<double(double)> func3 = [](double d)->double { return d / 4; };
cout << useF(func3, 11.11) << endl;
return 0;
}
2. bind包装器
bind也是一种函数包装器,也叫做适配器。它可以接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表,C++中的bind本质是一个函数模板。
调用bind的一般形式为:
auto newCallable = bind(callable, arg_list);
说明:
- callable:需要包装的可调用对象。
- newCallable:生成的新的可调用对象。
- arg_list:逗号分隔的参数列表,对应给定的callable的参数。当调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
用bind包装器绑定固定参数
int Plus(int a, int b)
{
return a + b;
}
int main()
{
//绑定固定参数
function<int(int)> func = bind(Plus, placeholders::_1, 10);
cout << func(2) << endl; //12
return 0;
}
用bind包装器调整传参顺序
对于下面Sub类中的sub成员函数,sub成员函数的第一个参数是隐藏的this指针,如果想要在调用sub成员函数时不用对象进行调用,那么可以将sub成员函数的第一个参数固定绑定为一个Sub对象。比如:
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};
int main()
{
//绑定固定参数
function<int(int, int)> func = bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);
cout << func(1, 2) << endl; //-1
return 0;
}
此时调用绑定后生成的可调用对象时,就只需要传入用于相减的两个参数了,因为在调用时会固定帮我们传入一个匿名对象给this指针。
如果想要将sub成员函数用于相减的两个参数的顺序交换,那么直接在绑定时将placeholders::_1和placeholders::_2的位置交换一下就行了。比如:
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};
int main()
{
//调整传参顺序
function<int(int, int)> func = bind(&Sub::sub, Sub(), placeholders::_2, placeholders::_1);
cout << func(1, 2) << endl; //1
return 0;
}
根本原因就是因为,后续调用新生成的可调用对象时,传入的第一个参数会传给placeholders::_1,传入的第二个参数会传给placeholders::_2,因此可以在绑定时通过控制placeholders::_n的位置,来控制第n个参数的传递位置。
无意义的绑定
int Plus(int a, int b)
{
return a + b;
}
int main()
{
//无意义的绑定
function<int(int, int)> func = bind(Plus, placeholders::_1, placeholders::_2);
cout << func(1, 2) << endl; //3
return 0;
}
3. bind包装器的意义
- 将一个函数的某些参数绑定为固定的值,让我们在调用时可以不用传递某些参数。
- 可以对函数参数的顺序进行灵活调整。