lambda表达式:[capture list] (params list) mutable exception-> return type { function body }
也叫匿名函数,以下统称为匿名函数。
对于一个函数而言,由4部分组成:
返回值类型 函数名(形参列表)
{
函数体;
}
而针对匿名函数而言,因为没有名字,我们在定义时即使用,它由以下几部分组成:
[捕获列表] (形参列表) ->返回值类型
{
函数体;
}
其中,形参列表与返回值类型中还存在可选参数,我们后面在讨论。
可以看到匿名函数与普通函数没有太大区别,唯一的区别就是多了一个捕获列表。
通常匿名函数的形式为 [](){}
的组合,而匿名函数的调用也与普通函数相似。普通函数的调用是通过 函数名() 的方式调用,而匿名函数没有函数名则是使用 匿名函数() 的方式调用的。
[](){}; // 匿名函数声明
[](){} (); // 匿名函数的调用
// 注:在使用匿名函数时主要有以上两种方式:
// 第一种称之为匿名函数的声明,是在匿名函数作为参数时使用,类比普通函数的函数名
// 第二种称之为匿名函数的调用,是在直接调用匿名函数的方法。
捕获列表
因为匿名函数的调用形式可以写成一行代码语句的形式,而在同一作用域下的局部变量是对所有语句都可见的,也就是说我们可以通过捕获列表来确定匿名函数调用时使用哪些外部变量。
捕获列表主要有以下几种形式:
捕获形式 | 说明 |
---|---|
[] | 不捕获任何外部变量(注意,可以使用全局变量和静态变量) |
[a, b…] | 默认以值得形式捕获指定的多个外部变量(用逗号分隔),如果引用捕获,需要显示声明(使用&说明符) |
[this] | 以值的形式捕获this指针 |
[=] | 以值的形式捕获所有外部变量(包括lambda所在类的this) |
[&] | 以引用形式捕获所有外部变量(同上) |
[=, &x] | 变量x以引用形式捕获,其余变量以传值形式捕获 |
[&, x] | 变量x以值的形式捕获,其余变量以引用形式捕获 |
按引用捕获相当于按引用传递。这里需要说明的是,按值捕获相当于实参传递形参的过程,捕获的变量在匿名函数内部生成一份等值的拷贝。
可选参数:
mutable指示符:用来说用是否可以修改按值捕获的变量。
需要注意的是,捕获列表存在的意义是捕获当前局部作用域内的对象,我们可以认为是一种特殊的函数参数列表。因为通过捕获列表所捕获的对象都是可以通过定义函数形参来代替的,而捕获列表仅仅是书写上更加方便和直观。而对于全局变量、静态变量、全局函数等这类全局作用域下,我们无需通过捕获列表就可直接使用。
依照现行C++11标准,在块作用域(block scope,可以简单理解为在{}之内的任何代码都是块作用域的)以外的lambda函数捕捉列表必须为空。因此这样的lambda函数除去语法上的不同以外,跟普通函数区别不大。而在块作用域中的lambda函数仅能捕捉父作用域中的自动变量,捕捉任何非此作用域或者是非自动变量(如静态变量等)都会导致编译器报错。——引用自《深入理解C++11:C++11新特性解析与应用>
int g_num = 10; // 全局变量
int func() { return 10; } // 全局函数
static int s_func() { return 10; } // 静态函数
int main()
{
static int s_num = 100; // 静态变量
cout << []() {return g_num + 10; }() << endl; // lambda+全局变量
cout << []() {return s_num + 10; }() << endl; // lambda+静态变量
cout << []() {return func() + 10; }() << endl; // lambda+全局函数
cout << []() {return s_func() + 10; }() << endl; // lambda+全局静态函数
return 0;
}
C++11中规定lambda表达式默认的operator() 是const方法,如果我们需要修改按值捕获的变量,我们需要添加mutable参数。
实例:按值捕获外部变量
int a = 10;
[=]() { cout << a << endl; }(); // 捕获外部变量a
[=]() mutable{ a = 20; cout << a << endl; }(); // 修改a
cout << a << endl;
// 输出 10 20 10
以上代码通过 [=]
可以捕获外部变量,并将其输出。当我们想要修改通过按值捕获的变量时,需要加上mutable
说明符。并且我们可以看到虽然在函数内部的 a
被修改成了 20,但是在匿名函数外的 a
的值没有被改变。
小结:按值捕获的变量默认是不可修改的,类似普通函数的const形参传递,而通过添加 mutable
说明符可以修改这些变量。不过,通过的按值捕获的变量是不会影响原来的外部变量的。
实例:按引用捕获外部变量
int a = 10;
[&]() { cout << a << endl; }();
[&]() { a = 20; cout << a << endl; }();
[&]() mutable{ a = 30; cout << a << endl; }();
cout << a << endl;
// 输出 10 20 30 30
通过输出我们可以看到,1. mutable
说明符只针对按值捕获的外部变量,按引用捕获的外部变量可以直接修改。 2. 修改按引用捕获的外部变量,原来的外部变量也会改变。
可选参数:
throw(类型) :表示匿名函数可以抛出指定类型的异常。
我们也可以使用 noexcept 异常规范来指示 lambda 表达式不会引发任何异常。
void MyFunction(int i) throw(); // 普通函数
[] () throw() {} (); // 匿名函数
void MyFunction(int i) noexcept; // 普通函数
[]() noexcept {} (); // 匿名函数
参数列表
普通函数参数列表:
int fun(int a, int b); // 申明
fun(1,2); // 使用
// 注:fun()函数需要实现
匿名函数参数列表:
[](int a,int b) {}; // 声明
[](int a, int b) {}(1, 2); // 使用
返回值
可以通过 ->type
的方式指定返回值类型。
实例:返回值
auto ret = [](auto a, auto b) {return a + b; }(1, 1.5);
cout << "ret = " << ret << endl; // 输出 2.5
// 声明返回类型为 int
auto retInt = [](auto a, auto b) ->int{return a + b; }(1, 1.5);
cout << "retInt = " << ret << endl; // 输出 2
Lambda 表达式的示例
auto fp = [捕获参数列表](函数参数列表) mutable throw(异常类型)->返回值类型 {函数体语句};
虽然匿名函数没有名字,但我们可以效仿函数指针的方式,使用指针去调用函数。由于匿名函数的类型比较复杂,这里我们可以使用auto
自动类型推演来实现。
auto fp = [](int i) {cout << i << endl; };
fp(1);
cout << typeid(fp).name() << endl; // class <lambda_b1aaf42c0977e0b63366721abda69325>
同时,匿名函数可以将另一个 匿名函数作为其自变量。或者是两个匿名函数嵌套。
实例:匿名函数嵌套与调用
auto f = [](int x) {return x; }; // x
cout << f(10) << endl;
auto ff = [](int x) {return [x](int y) {return x * y; }; }; // x * y
cout << ff(10)(20) << endl;
auto fff = [](int x) {return [x](int y) {return [x,y](int z) {return x * y * z; }; }; }; // x * y * z
cout << fff(10)(20)(30) << endl;
实例:匿名函数嵌套 f(n) = n * (n-1)
auto fact = [](int n) {return [n](int x) {return n * x; }(n - 1); }; // n * (n-1)
/*
auto fact = [](int n)
{
return
[n](int x)
{
return n * x;
}(n - 1);
};
*/
cout << fact(1) << endl; // 0
cout << fact(2) << endl; // 2
cout << fact(3) << endl; // 6
cout << fact(4) << endl; // 12
cout << fact(5) << endl; // 20
cout << fact(6) << endl; // 30
参考:C++11 中的std::function和std::bind 一文,我们可以将匿名函数的返回值声明为function<int(int)>
类型,这样就可以实现一个匿名函数作为参数被另一个匿名函数调用。
实例:
// 返回一个可调用对象(函数、函数对象、函数指针...)
auto addTwoInt = [](int x) -> function<int(int)> {
return [=](int y) { return x + y; };
};
auto higherorder = [](const function<int(int)>& f, int z) {
return f(z) * 2;
};
// Call the lambda expression that is bound to higherorder.
auto answer = higherorder(addTwoInt(7), 8);
// Print the result, which is (7+8)*2.
cout << answer << endl;
// 输出 30
其中第一个函数 addTwoInt
所指向的函数接收两个参数,作用是对两个数求和。同时返回值类型是一个可调用对象,而 higherorder
所指向的函数接收的参数为一个 可调用对象和一个普通内置类型, 因此 higherorder
可以调用 addTwoInt
。
更多高阶 Lambda 表达式请参考 Microsoft C++ Lambda 表达式的示例
除此之外,匿名函数还可以用于一些C++ 的 STL 库中提供的一些函数,类似如
- count_if:统计容器中符合某种条件的元素的个数。
- find_if :查找容器中第一个符合某种条件的元素。
- find_if_not():… 第一个不满足…
等模板函数,都能使用匿名函数,并且使用起来也很方便。
实例:find_if()
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int min = 5; // 大于 min 的第一个数
auto iter1 = std::find_if(begin(arr), end(arr), [min](int n) { return n > min; });
if (iter1 != end(arr))
std::cout << "在 arr[] 中第一个大于 " << min << "的数为:arr["
<< distance(begin(arr), iter1) << "]=" << *iter1 << endl;
// 输出:在 arr[] 中第一个大于 5的数为:arr[5]=6
auto iter2 = find_if_not(begin(arr), end(arr), [min](int n) { return n > min; });
std::cout << "在 arr[] 中第一个不大于 " << min << "的数为:arr["
<< distance(begin(arr), iter2) << "]=" << *iter2 << endl;
// 输出:在 arr[] 中第一个不大于 5的数为:arr[0]=1
2021.4.11跟新…
关于按值捕获对象
按值捕获对象时,因为捕获的对象是不可改变的(cosnt性质),因此我们只能使用对象中的常方法。
class Object
{
int num = 0;
public:
void show()
{
cout << "Object::Show ->num = " << num << endl;
}
void print() const
{
cout << "Object::num = " << num << endl;
}
};
按值捕获的对象只能调用常方法,按引用捕获的对象可以调用对象的非常方法。
int main()
{
auto f1 = [] {return 1; }; // 省略参数的写法
cout << f1() << endl;
Object obj;
auto f2 = [obj]() {obj.print(); };
//auto f3 = [=]() {obj.print(); };
auto f3 = [&obj]() {obj.show(); };
//auto f3 = [&]() {obj.show(); }
return 0;
}
关于匿名函数与auto的使用
已知,可以通过auto自动推导匿名函数的类型,从而给匿名函数(lambda表达式)一个临时的名字。
而如果我们想要给同一个lambda表达式取多个不同的名字,我们可以有以下几种方式:
int main()
{
auto f = [] {return 10; }; // 以此匿名表达式为例
第一种:// 需要多次重写匿名表达式。缺点:可能会发生手误写错的情况
auto f1 = [] {return 10; };
auto f2 = [] {return 10; };
auto f3 = [] {return 10; };
// ...
第二种:// 利用decltype() 定义同类型变量
decltype(f) df1 = f;
decltype(f) df2 = f;
decltype(f) df3 = f;
// ...
第三种:// 通过拷贝构造函数
auto cf1(f);
auto cf2(f);
auto cf3(f);
// ...
测试:// 输出的结果全部相同
cout << f() << "\n"
<< f1() << " " << f2() << " " << f3() << "\n"
<< df1() << " " << df2() << " " << df3() << "\n"
<< cf1() << " " << cf2() << " " << cf3() << "\n";
return 0;
}
通过以上示例我们可以看到,可以通过拷贝构造的方法定义新的同一个lambda表达式对象。而他们的赋值函数是不可用的。
auto f = [] {return 10; };
auto f2 = [] {return 10; };
f2 = f; //error
f2 = std::move(f); //error
关于lambda表达式与函数指针
在前面提到过,我们可以将lambda表达式看做是匿名的函数。而它本身除了拥有一个捕获列表之外,与普通函数几乎没什么区别。而我们的函数指针,函数对象,与lambda表达式都是可调用对象
有的说法称,C++11中的lambda表达式是一种函数对象的语法糖,因为捕获列表中捕获的参数就如同类的成员变量一般,而通过增加mutable关键字就可以在常方法修改类成员。而众所周知,在C++中不论是类成员还是普通变量或函数,都可以通过指针的方式去进行访问 。lambda表达式在没有捕获外部变量的时候,同样也是可以使用函数指针去指向的。
lambda的类型被定义为“闭包”(closure)的,而每个lambda表达式则会产生一个闭包类型的临时对象(右值)。因此,严格地讲,lambda函数并非函数指针。不过C++11标准却允许lambda表达是向函数指针的转换,但前提是lambda函数没有捕捉任何变量,且函数指针所示的函数原型,必须跟lambda函数有着相同的调用方式。——引用自《深入理解C++11:C++11新特性解析与应用>
typedef void (*pFunc)(int); // 函数指针类型
int main()
{
int a = 10;
auto f1 = [](int x) {cout << "x = " << x << endl; };
pFunc pf1 = f1; // lambda表达式退化成为函数指针
f1(a);
pf1(a);
return 0;
}