一、 lambda表达式
lambda表达式的引入
在C++98中,为了替代函数指针,C++设计出了仿函数,也称为函数对象。仿函数本质上就是一个普通的类,不过该类重载了函数调用操作符(),使得该类的对象可以像函数一样去使用。
虽然仿函数已经能够完全取代函数指针了,但是在一些场景下仍然有些难用。
// 商品类
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()); // 按照价格下降进行排序
return 0;
}
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了**Lambda表达式
**。
将上述排序仿函数使用lambda表达式的方式来实现:
lambda表达式的语法
💕 lambda表达式书写格式如下:
[capture-list] (parameters) mutable -> return-type { statement}
💕 lambda表达式各部分说明
[capture-list]
: 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[ ]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。(parameters)
:参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。mutable
: 默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。->returntype
:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。{statement}
:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:
- 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:
[]{}
; 该lambda函数不能做任何事情。
lambda表达式与函数对象
lambda表达式和仿函数一样,本质上也是一个可调用的函数对象,所以lambda表达式的使用方式和仿函数完全相同,但和仿函数不同的是,lambda表达式的类型是由编译器自动生成的,并且带有随机值,所以我们无法具体写出lambda表达式的类型,只能用auto进行推导。
int main()
{
auto add1 = [](int x, int y)->int { return x + y; };
cout << add1(1, 2) << endl;
auto add2 = [](int x, int y)->int
{
return x + y;
};
cout << add2(1, 1) << endl;
[] {}; // 最简单的lambda表达式
return 0;
}
其实lambda表达式本质上是底层通过编译器生成了一个匿名的函数对象,然后再通过这个函数对象来调用 operator()()
函数,从而完成调用,换句话说,lambda表达式底层就是通过替换为仿函数来完成的。
lambda表达式的捕捉列表
lambda表达式的捕捉列表可以捕捉父作用域中lambda表达式之前的所有变量,捕捉方式如下:
[var]
:表示值传递方式捕捉变量var。传值捕捉到的参数默认是被const 修饰的,因此不能再lambda表达式的函数体中修改他们,如果要修改,需要使用mutable修饰,但由于传值捕捉修改的是形参,所以一般我们也不会去修改它。[&var]
:表示引用传递捕捉变量var,通过引用传递捕捉,我们就可以在lambda表达式函数体中修改实参的值了。[&]
:表示引用传递捕捉所有父作用域中的变量(包括this)[=]
:表示值传递方式捕获所有父作用域中的变量
除了上面这几种捕捉方式之外,lambda表达式的捕捉列表还支持混合捕捉,如下:
💕 lambda 表达式有如下注意事项:
- 父作用域是指包含 lambda 函数的语句块,捕捉列表可以捕捉父作用域中位于 lambda 函数之前定义的所有变量;
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割;
比如:- [=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量;
- [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量;
- 捕捉列表不允许变量重复传递,否则就会导致编译错误;
- 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错;
- lambda 表达式之间不能相互赋值,即使看起来类型相同。
二、包装器
function包装器
function
是一个 可调用对象包装器, 也叫作适配器。它可以将函数指针、仿函数以及lambda表达式、成员函数等可调用对象进行包装,使他们具有相同的类型,包装器也可以像普通函数一样进行调用,包装器的本质还是仿函数。
在C++11标准中引入了 std::function
模板类,其定义在<function>
头文件中。
std::function<返回值类型(参数类型1, 参数类型2, ...)> f;
function 的使用类似于普通类,可以先定义一个function对象,然后将需要调用的函数赋值给该对象,也可以在定义function对象时直接使用可调用对象完成初始化,最后通过function对象进行函数调用。
方案一:
int f(int a, int b)
{
cout << "int f(int a, int b)" << endl;
return a + b;
}
struct Functor {
int operator()(int a, int b)
{
cout << "int operator()(int a, int b)" << endl;
return a + b;
}
};
// 先定义function对象,然后将需要调用的函数赋值给该对象
using func_t = function<int(int, int)>;
func_t func0 = f;
func_t func1 = Functor();
func_t func2 = [](int a, int b)->int {
cout << "[](int a, int b)->int{ return a + b; }" << endl;
return a + b;
};
方案二:
int main()
{
//int(*pf1)(int,int) = f;
function<int(int, int)> f1 = f;
function<int(int, int)> f2 = Functor();
function<int(int, int)> f3 = [](int a, int b)->int {
cout << "[](int a, int b)->int{ return a + b; }" << endl;
return a + b;
};
cout << f1(1, 2) << endl;
cout << f2(10, 20) << endl;
cout << f3(100, 200) << endl;
return 0;
}
经过上面 function 的包装,使得函数指针 f、仿函数 Functor、lambda 表达式以及类的静态成员函数具有了统一的类型 —— function<int(int, int)>
;类的普通成员函数我们也可以通过后面的绑定来让它的类型变为 function<int(int, int)>。
function封装类内成员函数
当function封装的是类内成员函数时,需要对该成员函数进行类域的声明,并且还需要在类域前面加一个取地址符。
- 静态成员函数没有this指针,所以function类实例化时不需要添加一个成员函数所属类的类型参数,在调用时也不需要传递一个成员函数所属类的对象。
- 非静态成员函数有this指针,所以需要传递成员函数所属类的对象并且进行类域声明。这里传递的是类的类型和类的对象。
class Plus {
public:
Plus(int rate = 2)
:_rate(rate)
{}
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return (a + b) * _rate;
}
private:
int _rate = 2;
};
int main()
{
// 静态成员函数
//function<int(int, int)> f1 = Plus::plusi;
function<int(int, int)> f1 = &Plus::plusi; // 上面的写法也是可以的
cout << f1(1, 2) << endl;
// 非静态成员函数
function<double(Plus, double, double)> f2 = &Plus::plusd;
cout << f2(Plus(), 1, 2) << endl;
function<double(Plus*, double, double)>f3 = &Plus::plusd;
Plus p;
cout << f3(&p, 1, 2) << endl;
return 0;
}
这里我们需要注意的是,因为this指针是不能显示的传递的。所以这里传递的并不是对应的this指针。
bind包装器
bind
也是一种包装器,也叫做适配器。它可以接受一个可调用对象,生成一个新的可调用对象来"适应"原对象的参数列表,C++中的bind本质是一个函数模板。
原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
fn
:可调用对象。args
:要绑定的参数列表、值或占位符。
调用bind的一般形式:auto newCallable = bind(callable,arg_list);
解释说明:
callable
:需要包装的可调用对象newCallable
:生成的新的可调用对象arg_list
:逗号分割的参数列表,对应给定的callable的参数。当调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
placeholders 是 C++11 引入的一个命名空间域,它包含了一些占位符对象(placeholder objects),用于在使用 bind 绑定函数时,指定某个参数需要在调用时再传递进来。其中参数可能是形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置,比如_1为newCallable的第一个参数,_2为第二个参数,以此类推。
此外,除了用auto接收包装后的可调用对象,也可以用function类型指明返回值和形参类型后接收包装后的可调用对象。
int Plus(int a, int b)
{
return a + b;
}
int main()
{
function<int(int, int)> func1 = bind(Plus, placeholders::_1, placeholders::_2);
auto func2 = bind(Plus, 5, 8);
cout << func1(1, 2) << endl;
cout << func2() << endl;
return 0;
}
bind函数同时也可以绑定类的成员函数和类的静态成员函数。
class Sub {
public:
int sub(int a, int b)
{
return a - b;
}
static int mul(int a, int b)
{
return a * b;
}
private:
};
💕 bind调整参数顺序
bind可以通过调整占位符的顺序来调整参数的顺序:
class Sub {
public:
int sub(int a, int b)
{
return a - b;
}
static int mul(int a, int b)
{
return a * b;
}
private:
};
int main()
{
function<int(int, int)> func3 = bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);
// 参数调换顺序
function<int(int, int)> func4 = bind(&Sub::sub, Sub(), placeholders::_2, placeholders::_1);
cout << func3(1, 2) << endl;
cout << func4(1, 2) << endl;
return 0;
}
💕 bind调整参数个数
bind可以在形参列表中直接绑定具体的函数对象,这样该参数就会自动传递,而不需要我们在调用函数时显示传递,并且也不需要我们在function的参数包中显示声明。这样我们就可以通过绑定让我们将类的普通成员函数和类的静态成员函数以及lambda表达式、函数指针一样定义为统一的类型了。
int main()
{
// 调整参数个数——非静态成员函数
function<int(Sub, int)> func5 = bind(&Sub::sub, placeholders::_1, 100, placeholders::_2);
cout << func5(Sub(), 20) << endl;
// 调整参数个数——静态成员函数
function<int(Sub, int)> func6 = bind(&Sub::mul, 100, placeholders::_2);
cout << func6(Sub(), 20) << endl;
return 0;
}