Loading

「学习笔记」Lambda 表达式

仅入门,主要为 C++11 和 C++14 标准,高版本的 Lambda 表达式更复杂,但对竞赛来说一般用不上很复杂的 Lambda 表达式。


平时我们使用 sort() 函数进行自定义排序,一般都会写一个函数,像下面这样的。

bool cmp(int a, int b) {
    return a > b;
}

int main() {
    int a[] = {1, 2, 3, 4, 5};
    sort(a, a + 5, cmp);
    for (int i = 0; i < 5; ++ i) {
        cout << a[i] << ' ';
    }
    return 0;
}

这个 cmp() 函数的作用就是将数组从大到小排序。

Lambda 表达式,可以直接用来代替 cmp() 函数,用 Lambda 表达式改写上面的代码。

int main() {
	int a[] = {1, 2, 3, 4, 5};
	auto cmp = [&](int a, int b) {
		return a > b;
	};
	sort(a, a + 5, cmp);
	// 或者是这样
	/*
	sort(a, a + 5, [&](int a, int b) {
		return a > b;
	});
	 */
	for (int i = 0; i < 5; ++ i) {
		cout << a[i] << ' ';
	}
	work();
	return 0;
}

尽管第二份代码看着怎么着也比第一份代码更高级,但是,两份代码最后的和输出都是一样的,Lambda 表达式起的作用与 cmp() 函数是一样的。

Lambda 表达式

Lambda 表达式的语法结构如下。

[捕获](参数) lambda 说明符 {函数体}

通过上面的例子,我们也会察觉,Lambda 表达式只是可以代替一些函数的工作,即 Lambda 表达式能完成的工作也可以被其他 C++ 语法完成,因此,它只是一种语法糖 但也很甜呀

捕获

顾名思义,就是捕获变量。

捕获是一个含有零或多个捕获符的逗号分隔列表,有两种默认捕获符。

  • & 以引用方式捕获变量。

  • = 以复制方式捕获变量。

如果捕获列表中只有一个 &,则默认是引用捕获,同理,如果捕获列表中只有一个 =,则默认是复制捕获,如果为空,则表示 Lambda 表达式的主体不会访问封闭范围内的变量。

当默认捕获符是 & 时,后继的简单捕获符不能以 & 开始。这里经过测试,只会弹出警告页面,但不会报错。

当变量以复制捕获时,如果没有 mutable 标识符,该变量是不能修改的。

下面用几份代码来试验一下。

int main() {
	int a = 2, b = 1, c = 3;
	auto it1 = [&] {
		b = a;
		a = c;
	};
	it1();
	cout << a << ' ' << b << '\n';
	return 0;
}

这份代码里面,默认捕获方式为引用捕获,因此 \(a\)\(b\) 都以引用捕获,\(a\)\(b\) 的值都被修改了。最后的输出为 3 2

int main() {
	int a = 2, b = 1;
	auto it1 = [&, a] {
		b = a;
	};
	it1();
	cout << a << ' ' << b << '\n';
	return 0;
}

在这份代码里面,这个捕获的含义为:默认变量以引用捕获,但是 \(a\) 以复制捕获,因此 \(b\) 以引用捕获,\(b\) 的值被修改了,而 \(a\) 以复制捕获,在这里面作为只读变量,值不会被修改。最后的输出为 2 2

int main() {
	int a = 2, b = 1;
	auto it1 = [&, &a] {
		b = a;
	};
	it1();
	cout << a << ' ' << b << '\n';
	return 0;
}

这份代码与第一份代码输出是一样的,只是会弹出警告。

int main() {
	int a = 2, b = 1;
	auto it1 = [=, &a] {
		a = b;
	};
	it1();
	cout << a << ' ' << b << '\n';
	return 0;
}

这份代码里面,默认是以复制捕获,\(a\) 以引用捕获,\(a\) 的值会被修改。最后输出 1 1

int main() {
	int a = 2, b = 1;
	auto it1 = [=, &a] {
		a = b;
		b = a;
	};
	it1();
	cout << a << ' ' << b << '\n';
	return 0;
}

报错,因为 \(b\) 是以复制捕获,没有 mutable 标识符是不可以被修改的。

int main() {
	int a = 2, b = 1;
	auto it1 = [] {
		a = b;
		b = a;
	};
	it1();
	cout << a << ' ' << b << '\n';
	return 0;
}

报错,it1 不能捕获 \(a\)\(b\)

int a = 2, b = 1, c = 3;

int main() {
	auto it1 = [] {
		a = b;
		b = c;
	};
	it1();
	cout << a << ' ' << b << '\n';
	return 0;
}
int a = 2, b = 1, c = 3;

int main() {
	auto it1 = [=] {
		a = b;
		b = c;
	};
	it1();
	cout << a << ' ' << b << '\n';
	return 0;
}

这两份代码的输出都是 1 3,因为全局变量默认以引用捕获,而捕获列表是针对捕获封闭空间的变量的,不会影响到全局变量。

参数

与函数的参数差不多,下面这两份代码中,最后的输出都是一样的。

int main() {
	int a = 2, b = 1;
	auto it1 = [](int c, int d) {
		c = d;
		return c;
	};
	cout << it1(a, b) << '\n';
	return 0;
}
int it1(int a, int b) {
	a = b;
	return a;
}

int main() {
	int a = 2, b = 1;
	cout << it1(a, b) << '\n';
	return 0;
}

lambda 说明符

lambda 说明符由说明符、异常说明、属性和尾随返回类型按前述顺序组成,每个组分均非必需,这里只会讲解说明符和尾随返回类型。

说明符

在 CCF 给我们开 C++17 之前,我们能用的说明符有 mutable,另外的几个说明符都需要更高版本的 C++,constexpr 从 C++17 起可以用,consteval 从 C++20 起可以用(constexprconsteval 不能同时使用),static 从 C++23 起才能使用(mutablestatic 不能同时使用,且使用 static 时,捕获列表必须为空)。

前面我们提到了,当变量以复制捕获时,如果没有 mutable 标识符,该变量是不能修改的,不提供说明符时复制捕获的对象在 Lambda 体内是 const 的。

mutable:允许函数体修改以复制捕获的对象,以及调用它们的非 const 成员函数。

使用了 mutable 说明符,我们就可以修改以复制捕获的变量了,在使用这个标识符时,前面的参数括号不要省略,即使里面为空,否则会弹出警告。

下面的这份代码不加 mutable 是会报错的,因为我们修改了以复制捕获的变量 \(b\),只要我们加上一个 mutable 说明符就可以了通过了。最后输出 3 1。(这里我们只是可以在函数体内修改以复制捕获的变量,但该变量本身不是以引用捕获的,因此它本身的值并不会改变)。

int main() {
	int a = 2, b = 1, c = 3;
	auto it1 = [=, &a] () mutable {
		a = b;
		b = c;
		a = b;
	};
	it1();
	cout << a << ' ' << b << '\n';
	return 0;
}

尾随返回类型

用于指定 Lambda 表达式的返回类型。若没有指定返回类型,则返回类型将被自动推断。具体的,如果函数体中没有 return 语句,返回类型将被推导为 void,否则根据返回值推导。

int main() {
	int a = 2, b = 1, c = 3;
	auto it1 = [=, &a] () mutable {
		a = b;
		b = c;
		a = b;
		return a;
	};
	cout << it1() << '\n';
	a = 2, b = 1, c = 3;
	auto it2 = [=, &a] () mutable -> int {
		a = b;
		b = c;
		a = b;
		return a;
	};
	cout << it2() << '\n';
	return 0;
}

代码中的 it1()it2() 效果是一样的,只是 it1() 是根据返回值自动判断的,it2() 是指定的 int 类型。

函数体

这个其实就是跟函数一模一样了,将你要运行的操作写在里面即可。

其他

关于 Lambda 表达式,我们发现上面的代码定义都是 auto 类型的,其实,除了 auto 类型,还可以是 function 类型的。

类模板 std::function ,定义于头文件 <functional>std::function 能存储、复制及调用任何可调用目标——函数、Lambda 表达式或其他函数对象,还有指向成员函数指针和指向数据成员指针。

在 Lambda 表达式中,存储需要表明返回类型和参数,如下。

int main() {
	int a = 2, b = 1, c = 3;
	function<int()> it1 = [=, &a] () mutable {
		return a;
	};
	cout << it1() << '\n';
	function<void(int)> it2 = [&] (int a) {
		b = a * a;
		cout << b << '\n';
	};
	it2(a);
	function<ll(int, int, int)> it3 = [] (int x, int y, int z) {
		return 1ll * x * y * z;
	};
	cout << it3(a, b, c) << '\n';
	return 0;
}

就像这样,在尖括号内,先表明返回类型,再在小括号内表明参数类型,有几个参数就写几个,是 int 就写 int,是 double 就写 double如果没有参数,也不能省略这个小括号

此外,Lambda 表达式被定义后,只能在该封闭范围内使用,否则会报错。

void work() {
	int a = 2, b = 1, c = 3;
	cout << it3(a, b, c) << '\n';
}

int main() {
	int a = 2, b = 1, c = 3;
	function<int()> it1 = [=, &a] {
		return a;
	};
	cout << it1() << '\n';
	function<void(int)> it2 = [&] (int a) {
		b = a * a;
		cout << b << '\n';
	};
	it2(a);
	function<ll(int, int, int)> it3 = [] (int x, int y, int z) {
		return 1ll * x * y * z;
	};
	cout << it3(a, b, c) << '\n';
	work();
	return 0;
}

这份代码就报错了,因为 it3() 实在 main() 里面定义的,work() 里面不能访问到,同理,在 work() 里面定义的 Lambda 表达式,main() 里面也不能访问到,当然,你也可以定义全局都可以使用的 Lambda 表达式,像这样。其实定义在全局就跟函数几乎一样了。

function<ll(int, int, int)> it3 = [] (int x, int y, int z) {
	return 1ll * x * y * z;
};

void work() {
	int a = 2, b = 1, c = 3;
	cout << it3(a, b, c) << '\n';
}

int main() {
	int a = 2, b = 1, c = 3;
	function<int()> it1 = [=, &a] {
		return a;
	};
	cout << it1() << '\n';
	function<void(int)> it2 = [&] (int a) {
		b = a * a;
		cout << b << '\n';
	};
	it2(a);
	cout << it3(a, b, c) << '\n';
	work();
	return 0;
}

关于 Lambda 表达式的应用,可以代替一些函数,像 sort() 里面的自定义比较函数。但总归而言,它只是一种语法糖,只是可以让代码更可读并且自带 inline,更高版本的 C++ 中,Lambda 表达式的使用会更复杂,想了解更多的可以来这里

参考资料

posted @ 2023-07-10 14:17  yi_fan0305  阅读(693)  评论(1编辑  收藏  举报