C++
14 May 2019

匿名函数从入门到入土

由《ISO/IEC 14882:2011》(C++11标准文档),C++C++11开始支持lambda表达式,俗称“匿名函数”。

因此,如果需要用g++进行编译,编译命令行应形如

$ g++ -o -std=c++11 code code.cpp

$ g++ -o -std=c++14 code code.cpp

匿名函数的原型如下

[capture] (parameters) mutable exception attribute -> return_type { body }

必须用方括号括起来的capture列表来开始一个lambda表达式的定义。

lambda函数的形参表比普通函数的形参表多了3条限制:

  • 参数不能有缺省值
  • 不能有可变长参数列表
  • 不能有无名参数

如果lambda函数没有形参且没有mutable(修饰变量,使其在const定义的函数中仍然可变)、exception(C++中异常的父类)或attribute声明,那么参数的空圆括号可以省略。但如果需要给出mutableexceptionattribute(用于编译器对源代码的优化)声明,那么参数即使为空,圆括号也不能省略。

如果函数体只有一个return语句,或者返回值类型为void,那么返回值类型声明可以被省略:

[capture](parameters){body}

举例如下:

[](int x, int y) {return x < y;} //返回表达式x < y的值
[](int &cxk) {++cxk;} //返回值自动设置为void
[](){++cxk;} //访问全局变量,形参void可被省略即
[]{++cxk;}

如果lambda函数体的形式是return expression,或者什么也没返回,或者所有返回语句用decltype都能检测到同一类型(我也不知道这是什么鬼玩意儿),那么返回值类型可以被省略。

如果需要显式指定返回值类型,则

[](int x, int y) -> int {return x + y;}

那么我们在构造sort函数时可以省略cmp函数而直接使用匿名函数

如将数组按降序排列:

不用匿名函数

 #include <bits/stdc++.h>
 using namespace std;
 const int N = 114514;
 int ikun[N];
 int cmp(int a, int b) { 
 	return a > b;
 }
 int main() {
 	int t;
 	cin >> t;
 	for(int i = 0; i < t; ++i) cin >> ikun[i];
 	sort(ikun, ikun + t, cmp);
 	for(int i = 0; i < t; ++i) cout << (i ? " " : "") << ikun[i];
 }

匿名函数

#include <bits/stdc++.h>
using namespace std;
const int N = 114514;
int ikun[N];
int main() {
	int t;
	cin >> t;
	for(int i = 0; i < t; ++i) cin >> ikun[i];
	sort(ikun, ikun + t, [](int a, int b) -> int {return a > b;}); // -> int 可省略
	for(int i = 0; i < t; ++i) cout << (i ? " " : "") << ikun[i];
}

可以少写个定义(虽然我不知道有什么用orz)

重点

外部变量的捕获

lambda函数可以捕获lambda函数外的具有automatic storage duration的变量,即函数的局部变量与函数形参变量。函数体与这些变量的集合合起来称做闭包。这些外部变量在声明lambda表达式时列在在方括号[]中。空的方括号表示没有外界变量被capture或者按照默认方式捕获外界变量。这些变量被传值捕获或者引用捕获。对于传值捕获的变量,默认为只读(这是由于lambda表达式生成的为一个函数对象,它的operator()成员缺省有const属性)。修改这些传值捕获变量将导致编译报错。但在lambda表达式的参数表的圆括号后面使用mutable关键字,就允许lambda函数体内的语句修改传值捕获变量,这些修改与lambda表达式(实际上是用函数对象实现)有相同的生命期,但不影响被传值捕获的外部变量的值。lambda函数可以直接使用具有static存储期的变量。如果在lambda函数的捕获列表中给出了static存储期的变量,编译时会给出警告,仍然按照lambda函数直接使用这些外部变量来处理。因此具有static存储期的变量即使被声明为传值捕获,修改该变量实际上直接修改了这些外部变量。编译器生成lambda函数对应的函数对象时,不会用函数对象的数据成员来保持被“捕获”的static存储期的变量。示例:

[]        //未定义变量.试图在Lambda内使用任何外部变量都是错误的.
[x, &y]   //x 按值捕获, y 按引用捕获.
[&]       //用到的任何外部变量都隐式按引用捕获
[=]       //用到的任何外部变量都隐式按值捕获
[&, x]    //x显式地按值捕获. 其它变量按引用捕获
[=, &z]   //z按引用捕获. 其它变量按值捕获
[this]	  //只传入this指针

上面是直接从维基百科抄过来的orz

下面是我的理解(举例论证)

例1

#include <bits/stdc++.h>
using namespace std;
int main() {
    auto i = 1145141919810;
    auto f = [](){cout << i << endl;};
    f();
}

此处[]中未定义任何外部变量的捕获方式,编译不通过

例2

#include <bits/stdc++.h>
using namespace std;
int main() {
    auto i = 114514;
    auto f = [=](){cout << i << endl;};
    i = 1919810;
    f();
}

由于传入的是i的值,此处输出114514

例3

#include <bits/stdc++.h>
using namespace std;
int main() {
    auto i = 114514;
    auto f = [&](){cout << i << endl;};
    i = 1919810;
    f();
}

由于传入的是i的引用,此处输出1919810

例4

比较以下两段代码

代码段1

#include <bits/stdc++.h>
using namespace std;
auto i = 114514;
int main() {
    auto f = [=](){cout << i << endl;};
    f();
    i = 1919810;
    f();
}

代码段2

#include <bits/stdc++.h>
using namespace std;
int main() {
    auto i = 114514;
    auto f = [=](){cout << i << endl;};
    f();
    i = 1919810;
    f();
}

分别输出

114514
1919810
114514
114514
  • 理解

代码段1中全局变量i并不是被捕获而输出,全局变量的输出更随其改动而变动

代码段2中局部变量i被捕获,在匿名函数中被记录为$i=114514$,之后对局部变量的改动不影响i在f中的值

例5

#include <bits/stdc++.h>
using namespace std;
long long i = 114514;
class cxk{
private:
	int basketball;
public:
	cxk();
	void music();
};
cxk::cxk() {
	basketball = 114514;
}
void cxk::music() {
	int rap = 1919810;
	auto f = [this](){cout << this->basketball << rap << endl;};
	f();
}
int main() {
    cxk nmsl;
    nmsl.music();
}

此处出现编译错误,因为在auto f = [this](){cout << this->basketball << rap << endl;};处引用了不属于类cxk的变量rap虽然他喜欢唱跳rap篮球

如果捕获方式改为=或&,则默认捕获this指针

例6

#include <bits/stdc++.h>
using namespace std;
map<int, int>mp;
int main() {
    mp[0] = 1, mp[1] = 2, mp[10] = 99;
    for(auto &x : mp) {
    	auto f = [=](){cout << x.first << " " << x.second << endl;};
    	f();
    }
}

捕获方式可改为&

广义捕获

C++14增加了广义捕获(Generalized capture)。即在捕获子句(capture clause)中增加并初始化新的变量,该变量不需要在lambda表达式所处的闭包域(enclosing scope)中存在;即使在闭包域中存在也会被新变量覆盖(override)。新变量类型由它的初始化表达式推导。一个用途是可以从闭包域中捕获只供移动的变量并使用它。

C++14还允许lambda函数的形参使用auto关键字作为其类型,这实质上是函数对象的operator()成员作为模板函数;并且允许可变参数模板。

例1

#include <bits/stdc++.h>
using namespace std;
long long i = 114514;
class cxk{
private:
	int basketball;
public:
	cxk();
	void music();
};
cxk::cxk() {
	basketball = 114514;
}
void cxk::music() {
    int rap = 1919;
	auto f = [this, rap = 1919810](){cout << this->basketball << rap << endl;};
	f();
}
int main() {
    cxk nmsl;
    nmsl.music();
}

例2

#include <bits/stdc++.h>
using namespace std;
long long i = 114514;
class cxk{
private:
	int basketball;
public:
	cxk();
	void music();
};
cxk::cxk() {
	basketball = 114514;
}
void cxk::music() {
	int rap = 1919;
	auto f = [this](auto rap = 1919810){cout << this->basketball << rap << endl;};
	f(rap);
}
int main() {
    cxk nmsl;
    nmsl.music();
}

可变参数模板

因为本人是菜鸡,没写过可变参数模板,略


Tags:
0 comments