在数理逻辑或计算机科学领域中 lambda 是被用来表示一种匿名函数,这种匿名函数代表了一种所谓的λ演算(lambda calculus)。
int main()
{
int girls=3,boys=4;
auto totalChild=[](int x,int y)->int{return x+y;};
return totalChild(girls,boys);
}
该函数接受两个参数(int x, int y)
,并且返回其和。直观地看,lambda函数跟普通函数相比不需要定义函数名,取而代之的多了一对方括号([])。此外,lambda函数还采用了追踪返回类型的方式声明其返回值。
其余方面看起来则跟普通函数定义一样。
lambda函数的一般形式
而通常情况下,lambda 函数的语法定义如下:
[capture](parameters)mutable->return-type{statement}
其中
- [capture] : 捕捉列表。捕捉列表总是出现在 lambda 函数的开始处。事实上,[ ] 是lambda 引出符。编译器根据该引出符判断接下来的代码是否是lambda函数。捕捉列表能够捕捉上下文中的变量以供 lambda 函数使用。
- (parameters) :参数列表。与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号()一起省略。
- mutable :mutable修饰符。默认情况下,lambda 函数总是一个 const 函数,mutable 可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->return-type:返回类型。用追踪返回类型形式声明函数的返回类型。出于方便,不需要返回值的时候也可以连同符号->一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。
- {statement}:函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。
在lambda函数的定义中,参数列表和返还类型都是可选的部分,而捕捉列表和函数体都
可能为空。那么在极端情况下,C++11中最为简略的lambda函数只需要声明为
[]{};
就可以了。不过理所应当地,该lambda函数不能做任何事情。
捕获列表
int main()
{
[]{};//最简lambda函数
int a=3;
int b=4;
[=]{return a+b;}; //省略了参数列表与返回类型,返回类型由编译器推断为int
auto fun1=[&](int c){b=a+c;}; //省略了返回类型,无返回值
auto fun2=[=,&b](int c)->int{return b+=a+c;}; //各部分都很完整的lambda函数
}
直观地讲,lambda函数与普通函数可见的最大区别之一,就是 lambda 函数可以通过捕捉列表访问一些上下文中的数据。
具体地,捕捉列表描述了上下文中哪些的数据可以被 lambda 使用,以及使用方式(以值传递的方式或引用传递的方式)。
int main()
{
int boys=4,int girls=3;
auto totalChild=[girls,&boys]()->int{return girls+boys;};
return totalChild();
}
在上面代码中,我们使用了捕捉列表捕捉上下文中的变量 girls、boys。与之前的代码相比,函数的原型发生了变化,即 totalChild
不再需要传递参数。这个改变看起来平淡无奇,此时 girls
和 boys
可以视为 lambda 函数的一种初始状态,lambda 函数的运算则是基于初始状态进行的运算。这与函数简单基于参数的运算是有所不同
的。
语法上,捕捉列表由多个捕捉项组成,并以逗号分割。捕捉列表有如下几种形式:
-
[var]表示值传递方式捕捉变量var。
-
[=]表示值传递方式捕捉所有父作用域的变量(包括this)。
多次调用的lambda表达式不要用值捕获,变量传递进来后相当于常量不更新。
-
[&var]表示引用传递捕捉变量var。
-
[&]表示引用传递捕捉所有父作用域的变量(包括this)。
-
[this]表示值传递方式捕捉当前的this指针。
注意
父作用域:enclosing scope,这里指的是包含lambda函数的语句块,在上面的代码中,即main函数的作用域。
通过一些组合,捕捉列表可以表示更复杂的意思。比如:
- [=,&a,&b] 表示以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量。
- [&,a,this] 表示以值传递的方式捕捉变量a和this,引用传递方式捕捉其他所有变量。
不过值得注意的是,捕捉列表不允许变量重复传递。下面一些例子就是典型的重复,会导致编译时期的错误。
- [=,a]这里=已经以值传递方式捕捉了所有变量,捕捉a重复。
- [&,&this]这里&已经以引用传递方式捕捉了所有变量,再捕捉this也是一种重复。
在块作用域(block scope,可以简单理解为在{}之内的任何代码都是块作用域的)以外的lambda函数捕捉列表必须为空,在块作用域中的lambda函数仅能捕捉父作用域中的自动变量,捕捉任何非此作用域或者是非自动变量(如静态变量等)都会导致编译器报错。
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 Changi(tax_rate);
auto Changi2 = [tax_rate](float price)->float
{
return price*(1-tax_rate/100);
};
float purchased = Changi(3699);
float purchased2 = Changi2(2899);
}
在上面代码中,lambda函数捕捉了tax_rate
变量,而仿函数则以tax_rate
初始化类。其他的,如在参数传递上,两者保持一致。可以看到,除去在语法层面上的不同,lambda和仿函数却有着相同的内涵——都可以捕捉一些变量作为初始状态,并接受参数进行运算。
而事实上,仿函数是编译器实现lambda的一种方式。在现阶段,通常编译器都会把 lambda 函数转化为成为一个仿函数对象。
因此,在C++11中,lambda可以视为仿函数的一种等价形式了,或者更动听地说,lambda是仿函数的“语法甜点”。
注意
有的时候,我们在编译时发现lambda函数出现了错误,编译器会提示一些构造函数等相关信息。这显然是由于lambda的这种实现方式造成的。理解了这种实现,用户也就能够正确理解错误信息的由来。
lambda 的基础使用
extern int z;
extern float c;
void Calc(int &, int, float &, float);
void TestCalc()
{
int x, y = 3;
float a, b = 4.0;
int success = 0;
auto validate = [&]() -> bool
{
if ((x == y + z) && (a == b + c))
return 1;
else
return 0;
};
Calc(x, y, a, b);
success += validate();
y = 1024;
b = 1e13;
Calc(x, y, a, b);
success += validate();
}
在没有lambda函数之前,通常需要在TestCalc外声明同样一个函数,并且把TestCalc中的变量当作参数进行传递。出于函数作用域及运行效率考虑,这样声明的函数通常还需要加上关键字static和inline。
相比于一个传统意义上的函数定义,lambda函数在这里更加直观,使用起来也非常简便,代码可读性很好,效果上,lambda函数则等同于一个“局部函数”。
注意
局部函数(local function,即在函数作用域中定义的函数),也称为内嵌函数(nested function)。局部函数通常仅属于其父作用域,能够访问父作用域的变量,且在其父作用域中使用。
C/C++语言标准中不允许局部函数存在(不过一些其他语言是允许的,比如FORTRAN),C++11标准却用比较优雅的方式打破了这个规则。因为事实上,lambda可以像局部函数一样使用。
lambda 扩展
// 常量的值由它自己初始化状态决定
int Prioritize(int);
int AllWorks(int times)
{
int i;
int x;
try
{
for (i = 0; i < times; i++)
x += Prioritize(i);
}
catch (...)
{
x = 0;
}
const int y = [=]
{
int i, val;
try
{
for (i = 0; i < times; i++)
val += Prioritize(i);
}
catch (...)
{
val = 0;
}
return val;
}();
}
lambda的类型
从C++11标准的定义上可以发现,lambda的类型被定义为“闭包”(closure)的类 ,而每个lambda表达式则会产生一个闭包类型的临时对象(右值)。
因此,严格地讲,lambda函数并非函数指针。不过C++11标准却允许lambda表达是向函数指针的转换,但前提是lambda函数没有捕捉任何变量,且函数指针所示的函数原型,必须跟lambda函数有着相同的调用方式。
int main()
{
int girls = 3, boys = 4;
auto totalChild = [](int x, int y) -> int
{ return x + y; };
typedef int (*allChild)(int x, int y);
typedef int (*oneChild)(int x);
allChild p;
p = totalChild;
oneChild q;
q = totalChild; // 编译失败,参数必须一致
decltype(totalChild) allPeople = totalChild; // 需通过decltype获得lambda的类型
decltype(totalChild) totalPeople = p; // 编译失败, 指针无法转换为lambda
return 0;
}
int main()
{
int val;
// 编译失败,在const的lambda中修改常量
auto const_val_lambda = [=](){ val = 3; };
// 非const的lambda,可以修改常量数据
auto mutable_val_lambda = [=]() mutable{ val = 3; };
// 依然是const的lambda,不过没有改动引用本身
auto const_ref_lambda = [&]{ val = 3; };
// 依然是const的lambda,通过参数传递val
auto const_param_lambda = [&](int v){ v = 3; };
const_param_lambda(val);
return 0;
}
lambda 与 STL
#include <vector>
#include <algorithm>
using namespace std;
vector<int> nums;
vector<int> largeNums;
const int ubound = 10;
inline void LargeNumsFunc(int i)
{
if (i > ubound)
largeNums.push_back(i);
}
void Above()
{
// 传统的for循环
for (auto itr = nums.begin(); itr != nums.end(); ++itr)
{
if (*itr >= ubound)
largeNums.push_back(*itr);
}
// 使用函数指针
for_each(nums.begin(), nums.end(), LargeNumsFunc);
// 使用lambda函数和算法for_each
for_each(nums.begin(), nums.end(), [=](int i)
{
if (i > ubound)
largeNums.push_back(i);
});
}
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;
extern vector<int> nums;
void TwoCond(int low, int high)
{
// 传统的for循环
for (auto i = nums.begin(); i != nums.end(); i++)
if (*i >= low && *i < high)
break;
// 利用了3个内置的仿函数,以及非标准的compose2
find_if(nums.begin(), nums.end(),
compose2(logical_and<bool>(),
bind2nd(less<int>(), high),
bind2nd(greater_equal<int>(), low)));
// 使用lambda函数
find_if(nums.begin(), nums.end(), [=](int i)
{ return i >= low && i < high; });
}
#include <vector>
#include <algorithm>
#include <iostream>
#include <functional>
using namespace std;
vector<int> nums;
void Add(const int val)
{
auto print = [&]
{
for (auto s : nums)
{
cout << s << '\t';
}
cout << endl;
};
// 传统的for循环方式
for (auto i = nums.begin(); i != nums.end(); ++i)
{
*i = *i + val;
}
print();
// 试一试for_each及内置仿函数
for_each(nums.begin(), nums.end(), bind2nd(plus<int>(), val));
print();
// 实际这里需要使用STL的一个变动性算法:transform
transform(nums.begin(), nums.end(), nums.begin(), bind2nd(plus<int>(), val));
print();
// 不过在lambda的支持下,我们还是可以只使用for_each
for_each(nums.begin(), nums.end(), [=](int &i)
{ i += val; });
print();
}
int main()
{
for (int i = 0; i < 10; i++)
{
nums.push_back(i);
}
Add(10);
return 1;
}
lambda表达式的开销
C++ Lambda表达式的开销取决于许多因素,例如Lambda的实现方式、编译器的优化能力、Lambda所在的上下文等等。
Lambda表达式在编译时会被转换为一个匿名的函数对象,并且该函数对象可能需要在运行时动态分配和释放内存。因此,Lambda表达式可能会带来一定的运行时开销和内存开销。
在使用Lambda表达式时,如果Lambda表达式只是简单的函数调用或者循环中的一小段代码,那么它的开销可能不会对程序的性能产生显著的影响。但是,如果Lambda表达式被频繁调用或者嵌套在多个循环中,那么它的开销可能会导致程序的性能下降。
另外,Lambda表达式可能会引入额外的编译时间开销,因为编译器需要解析和生成Lambda表达式的代码。但是,现代编译器通常会对Lambda表达式进行优化,以减少其编译时间和运行时开销。
因此,在使用Lambda表达式时,需要根据实际情况进行权衡,考虑Lambda表达式的使用场景和性能影响,并且可以通过一些技巧(如使用std::function、将Lambda表达式定义为静态变量等)来减少Lambda表达式的开销。
总结
C++ 11 中 lambda与仿函数要做好取舍,lambda只能捕获父作用域中的变量,仿函数在某些情况下又很繁杂。
标签:11,return,函数,nums,int,C++,捕捉,lambda From: https://www.cnblogs.com/Cycas/p/17649252.html