文章目录
1. 谓词 (predicate)
在引入lambda表达式之前,先引入 谓词(predicate) 的概念。声明在 <algorithm> 头文件中的 sort 函数引入了第三个参数,除了常见的 first 和 last 参数外,第三个参数是 comp,就是一个谓词。
![](https://i-blog.csdnimg.cn/blog_migrate/db159350f033abde7641a53c4ed989ca.png)
查看 函数文档 中对 comp 的描述,具体的排序方法和准则见 sort函数及其元素排序方式定制:
Binary function that accepts two elements in the range as arguments, and returns a
value convertible to bool. The value returned indicates whether the element passed
as first argument is considered to go before the second in the specific strict weak
ordering it defines.
The function shall not modify any of its arguments.
This can either be a function pointer or a function object.
可以看到描述中提到 comp 有两个形参,返回一个bool类型。这与 谓词是一个可调用的表达式,其返回结果是一个能用做条件的值(意为是或不是) 相契合。标准库中所定义的谓词分为两类,一元谓词(unary predicate,意味着他们只接受单一参数) 和 二元谓词(binary predicate,有两个参数),这里的comp即是二元谓词,输入是两个RandomAccessIterator,输出是一个bool类型(可作为条件值)。
此外,提到谓词,可以将其理解为仿函数,为了融入 STL 体系中,能够对仿函数进行适配的话,势必要继承 unary_function 或 binary_function,以继承其中的 typedef 输入参数类型和返回值类型的别名。具体的解析可见:STL仿函数 :: 2. 仿函数能够进行配接的条件。
2. lambda 表达式概述
如上,根据 STL 体系,算法只能接受 一元谓词 和 二元谓词 (继承unary_function或binary_function),所以传递给算法的谓词参数必须是一个或两个。但是,有时我们需要进行的操作需要有更多的参数参与,超出了不超过两个参数的限制。
对于此种情形,我们可以向算法传入一个可调用对象(比如重载了function call operator的类型,即如果e是一个可调用表达式,则使用e(args)创建一匿名/临时对象作为算法的实参传入,在算法内部调用()进行操作,其args是参数列表,可打破两个参数的限制,也就是仿函数的概念)作为谓词。
对于可调用对象总共有四种:①函数;②函数指针;③重载了函数调用运算符的类;④lambda表达式。函数可调用没什么可说的;函数指针示例:bool (*pf) (const string&, const string&);
,其中(*pf)两边的括号不可省略,若是省略就变为返回bool指针类型的函数了,具体实例见 c++中的函数指针转函数参数;重载了函数调用运算符的类,就是STL仿函数的概念,示例见 STL仿函数;
lambda 表达式也是为了打破参数限制问题应运而生的,其是 C++11 中新引入的特性。
lambda 表达式表示一个可调用的代码单元,用于定义并创建匿名的函数对象,以简化编程工作,其编译器实现方式是创建一个仿函数对象。可以将其理解为一个未命名的匿名内联函数。
3. lambda 表达式语法
与任何函数类似,一个 lambda 表达式具有一个返回类型 return type、一个参数列表 parameter list 和一个函数体 function body。但与函数不同,lambda 可能定义在函数内部,所以还有一 capture list(捕获列表),是函数中定义的局部变量的列表。
[ c a p t u r e l i s t ] ( p a r a m e t e r l i s t ) ( o p t i o n a l m u t a b l e / e x c e p t i o n ) − > r e t u r n t y p e { f u n c t i o n b o d y } [capture \ list] \ (parameter\ list) (optional \ mutable/exception)\ -> \ return \ type \ \{ function \ body\} [capture list] (parameter list)(optional mutable/exception) −> return type {function body}
lambda 表达式必须使用尾置返回类型(trailing return type,对于返回类型较复杂的函数最有效,比如返回数组指针或引用,详见c++ primer P 206 P206 P206),可以忽略参数列表和返回类型,忽略参数列表则指接收空参数,忽略返回类型则返回类型从返回表达式的类型推断而来,若是包含单个return之外的任何语句,如条件分支等,则编译器假定此lambda返回void(此时必须使用后置返回类型来指定返回类型),同时使用auto修饰符做左值。
3.1 捕获列表 []
捕捉列表总是出现在lambda函数的开始处。事实上,[]是lambda引出符,编译器根据该引出符判断接下来的代码是否是lambda函数(所以不能够省略参数列表)。捕捉列表能够捕捉上下文中的变量以供lambda使用,变量的捕获方式也分为值和引用,具体有以下形式:
- []:没有任何函数对象参数。
- [a]:将 a 按值进行传递。按值进行传递时,函数体内不能修改传递进来的 a 的拷贝,因为默认情况下 lambda 函数是 const 的,
mutable
关键字可以取消其常量性,要修改传递进来的拷贝,在参数列表后添加mutable
关键字(auto f = [v1] () mutable { return ++ v1;
})。 - [&a]:将 a 按引用进行传递。
- [a, &b]:将 a 按值传递,b 按引用进行传递。
- [this]:函数体内可以使用 lambda 所在类中的成员变量,值/拷贝捕获。
exception
修饰符,声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw(int)。
建议:尽量保持lambda的变量捕获简单化。
以上是在捕获列表中对来自函数的局部变量的显示列出,也即显示捕获,还可以让编译器根据lambda function body中的代码推断要使用哪些变量,也就是隐式捕获。在捕获列表中写&或=,&告诉编译器默认采用捕获引用方式,=表示默认采用值捕获方式。也可指定一部分采用默认方式,特定采用非默认方式的混合显示捕获和隐式捕获的方式,且特定/显示捕获方式与默认/隐式捕获方式必须不同:
- [=]:函数体内可以使用 Lambda 所在范围内所有可见的局部变量(包括 Lambda 所在类的 this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
- [&]:函数体内可以使用 Lambda 所在范围内所有可见的局部变量(包括 Lambda 所在类的 this),并且是引用传递方式(相当于是编译器自动为我们按引用传递了所有局部变量)。
- [=, &a, &b]:除 a 和 b 按引用进行传递外,其他参数都按值进行传递,混合捕获。
- [&, a, b]: 除 a 和 b 按值进行传递外,其他参数都按引用进行传递,混合捕获。
![](https://i-blog.csdnimg.cn/blog_migrate/9d1cbec9c18fb51673c14d7a1bd749c9.png)
3.2 指定返回类型
- 若是 lambda 表达式 function body 函数体中包含 return 之外的任何语句则编译器假定此lambda返回void,也即是不能判断返回类型是何种类型;
- 若是只含return语句,则编译器可以对返回类型进行推断。
见如下示例代码:
//无需指定返回类型,返回类型可以根据条件运算符的类型推断出来
transform(vi.begin(), vi.end(), vi.begin(),
[](int i) { return i > 0 ? i : -i; });
//需使用尾置法指定返回类型,因为不能推断出lambda的返回类型的示例
//虽然看起来和上面是等价的,但是会发生编译错误,编译器推断其返回类型是void,但却返回了int类型
transform(vi.begin(), vi.end(), vi.begin(),
[](int i) { if(i > 0) return i; else return -i; });
//以上代码使用尾置返回类型修改后,即可通过编译
transform(vi.begin(), vi.end(), vi.begin(),
[](int i) -> int
{ if(i > 0) return i; else return -i; });
4. lambda 表达式和 functor 仿函数
仿函数(functor)作为 STL 六大组件(components)中的一个,也称函数对象(function object),其实就是重载了函数调用运算符,即operator()操作符的 struct / class。在代码层面使用起来像是函数,但是其本身是一实例化对象。以下是一示例:
class _functor {
public:
int operator() (int x, int y) { return x + y; }
};
int main() {
int girls = 3, boys = 4;
_functor totalChild;
return totalChild(girls, boys); //12
}
lambda表达式本身和仿函数除去在语法层面上的不同,其本质都是捕捉一些变量作为初始状态,即作为其中的构造过程中初始化的成员,利用这些状态并接受一定参数进行运算。如下所示:
class AirportPrice {
private:
float _dutyfreerate;
public:
AirportPrice(float rate): _dutyfreerate(rate){};
float operator() (float price) {
return price * (1 - _dutyfreerate / 100);
}
};
int main() {
float tax_rate = 5.5f;
//仿函数实现
AirportPrice calPrice_functor(tax_rate);
//lambda表达式实现
auto calPrice_lambda = [tax_rate](float price) -> float { return price * (1 - tax_rate / 100); };
float order1_sum = calPrice_functor(1000);
float order2_sum = calPrice_lambda(1000);
//两个结果一样
}
而事实上,正如上文第2节红色字中所提到的一样,仿函数是编译器实现 lambda 的一种方式。在现阶段,通常编译器都会把 lambda 转化为一个仿函数对象。我们可以通过下图直观的了解 lambda 和仿函数是如何等价的。
![](https://i-blog.csdnimg.cn/blog_migrate/9d0eca6c1d0ebc9af93b07504a0e9ed8.png)
5. 参考资料
- 《C++ primer》第5版
- lambda表达式与仿函数
- C++仿函数和Lambda表达式
- C++之Lambda表达式