Lambda 表达式
构造闭包:能够捕获作用域中的变量的无名函数对象。since C++11
语法
[ captures ] ( params ) specs requires(optional) { body }
|
(1) | |
[ captures ] { body }
|
(2) | (until C++23) |
[ captures ] specs { body }
|
(2) | (since C++23) |
[ captures ] < tparams > requires(optional) ( params ) specs requires(optional) { body }
|
(3) | (since C++20) |
[ captures ] < tparams > requires(optional) { body }
|
(4) | (since C++20) (until C++23) |
[ captures ] < tparams > requires(optional) specs { body }
|
(4) | (since C++23) |
- 完整声明。
- 省略形参列表:函数不接收实参,如同形参列表是 ()。
- 与 1 相同,但指定泛型 lambda 并显式提供模板形参列表。
- 与 2 相同,但指定泛型 lambda 并显式提供模板形参列表。
解释
- captures - 包含零或更多个捕获符的逗号分隔列表,可以 默认捕获符(capture-default) 起始。 有关捕获符的详细描述,见下文。 如果变量满足下列条件,那么 lambda 表达式在使用它前不需要先捕获:
- tparams - (角括号中的)模板形参列表,用于为泛型 lambda 提供各模板形参的名字(见下文的 ClosureType::operator())。与在模板声明中相似,模板形参列表可以后附 requires 子句,它指定各模板实参上的约束。 模板形参列表不能为空(不允许 <>)。
- params - 形参列表,如在具名函数中。
- specs - 由 specifiers、exception、attr 和 trailing-return-type 按顺序组成,每个组分均非必需
- specifiers - 可选的说明符的序列。不提供说明符时复制捕获的对象在 lambda 函数体内是 const 的。可以使用下列说明符:
- exception - 为闭包类型的 operator() 提供动态异常说明或 (C++20 前) noexcept 说明符
- attr - 为闭包类型的函数调用运算符或运算符模板的类型提供属性说明。这样指定的任何属性均属于函数调用运算符或运算符模板的类型,而非其自身。(例如不能使用 [[noreturn]])
- trailing-return-type - -> 返回类型,其中 返回类型 指定返回类型。如果没有 尾随返回类型,那么闭包的 operator() 的返回类型从 return 语句推导,如同对于声明返回类型为 auto 的函数的推导一样。
- requires - (C++20 起)向闭包类型的 operator() 添加约束
- body - 函数体
当以 auto 为形参类型或显式提供模板形参列表 (C++20 起)时,该 lambda 是泛型 lambda。(C++14 起)
Lambda 捕获
捕获 是一个含有零或更多个捕获符的逗号分隔列表,可以 默认捕获符 开始。默认捕获符只有
- &(以引用隐式捕获被使用的自动变量)和
- =(以复制隐式捕获被使用的自动变量)。
当出现任一默认捕获符时,都能隐式捕获当前对象(*this
)。若隐式捕获它,则始终以引用捕获,即使默认捕获符是 =。当默认捕获符为 = 时,*this
的隐式捕获被弃用。 (C++20 起)
捕获 中单独的捕获符的语法是:
标识符 | (1) | |
标识符 ...
|
(2) | |
标识符 初始化器 | (3) | (C++14 起) |
& 标识符
|
(4) | |
& 标识符 ...
|
(5) | |
& 标识符 初始化器
|
(6) | (C++14 起) |
this
|
(7) | |
* this
|
(8) | (C++17 起) |
... 标识符 初始化器
|
(9) | (C++20 起) |
& ... 标识符 初始化器
|
(10) | (C++20 起) |
- 简单的以复制捕获
- 作为包展开的简单的以复制捕获
- 带初始化器的以复制捕获
- 简单的以引用捕获
- 作为包展开的简单的以引用捕获
- 带初始化器的以引用捕获
- 当前对象的简单的以引用捕获
- 当前对象的简单的以复制捕获
- 初始化器为包展开的以复制捕获
- 初始化器为包展开的以引用捕获
当默认捕获符是 & 时,后继的简单捕获符不能以 & 开始。
struct S2 { void f(int i); };
void S2::f(int i)
{
[&]{}; // OK:默认以引用捕获
[&, i]{}; // OK:以引用捕获,但 i 以值捕获
[&, &i] {}; // 错误:以引用捕获为默认时的以引用捕获
[&, this] {}; // OK:等价于 [&]
[&, this, i]{}; // OK:等价于 [&, i]
}
当默认捕获符是 = 时,后继的简单捕获符必须以 & 开始,或者为 *this (C++17 起) 或 this (C++20 起)。
struct S2 { void f(int i); };
void S2::f(int i)
{
[=]{}; // OK:默认以复制捕获
[=, &i]{}; // OK:以复制捕获,但 i 以引用捕获
[=, *this]{}; // C++17 前:错误:无效语法
// C++17 起:OK:以复制捕获外围的 S2
[=, this] {}; // C++20 前:错误:= 为默认时的 this
// C++20 起:OK:同 [=]
}
任何捕获符只可以出现一次:
struct S2 { void f(int i); };
void S2::f(int i)
{
[i, i] {}; // 错误:i 重复
[this, *this] {}; // 错误:"this" 重复 (C++17)
}
仿函数
在C++11之前,STL中的一些算法需要使用一种函数对象---仿函数(functor);其本质是重新定义和成员函数 operator() ,使其使用上很像普通函数,其实,细心的我们已经发现,Lambda函数与仿函数似乎有一些默契。 如下例子:折扣
class Price
{
private:
float _rate;
public:
Price(float rate):_rate(rate){}
float operator()(float price)
{
return price*(1 - _rate / 100);
}
};
int main(int argc, char* argv[])
{
float rate=5.5f;
Price c1(rate);
auto c2 = [rate](float price)->float {return price*(1 - rate / 100); };
float p1 = c1(3699); //仿函数
float p2 = c2(3699); //Lambda函数
return 0;
}
仿函数以rate初始化,Lambda捕捉rate变量,参数传递上,两者一致。
事实上,仿函数就是实现Lambda函数一种方式,编译器通常会把Lambda函数转换为一个仿函数对象,但是仿函数的语法却给我们带来了很大的便捷。
在C++11中,Lambda函数被广泛使用,很多仿函数被取代。
示例
int main(int argc, char* argv[])
{
int a = 5, b = 7;
auto total = [](int x, int y)->int {return x + y; }; //接受两个参数
cout << total(a, b)<<endl; //12
auto fun1 = [=] {return a + b; }; //值传递捕捉父作用域变量
cout << fun1() << endl; //12
auto fun2 = [&](int c) {b = a + c; a = 1; }; //省略了返回值类型,引用捕获所有
fun2(3);
cout << a <<" "<< b << endl; //1 8
a = 5; b = 7; //被修改后,重新赋值
auto fun3 = [=, &b](int c) mutable {b = a + c; a = 1; }; //以值传递捕捉的变量,在函数体里如果要修改,要加mutaple,因为默认const修饰
fun3(3);
cout << a << " " <<b<< endl; //5 8
a = 5; b = 7; //被修改后,重新赋值
auto fun4 = [=](int x, int y) mutable->int {a += x; b += y; return a + b; };
int t = fun4(10, 20);
cout << t << endl; //42
cout << a <<" "<< b << endl; //5 7
return 0;
}
块作用域以外的Lambda函数捕捉列表必须为空,因此这样的函数除了语法上的不同,和普通函数区别不大。
块作用域以内的Lambda函数仅能捕捉块作用域以内的自动变量,捕捉任何非此作用域或非自动变量(静态变量),都会引起编译器报错。
下面示例演示如何传递 lambda 给泛型算法,以及 lambda 表达式所产生的对象能如何存储于 std::function 对象。
#include <vector>
#include <iostream>
#include <algorithm>
#include <functional>
int main()
{
std::vector<int> c = {1, 2, 3, 4, 5, 6, 7};
int x = 5;
c.erase(std::remove_if(c.begin(), c.end(), [x](int n) { return n < x; }), c.end());
std::cout << "c: ";
std::for_each(c.begin(), c.end(), [](int i){ std::cout << i << ' '; });
std::cout << '\n';
// the type of a closure cannot be named, but can be inferred with auto
// since C++14, lambda could own default arguments
auto func1 = [](int i = 6) { return i + 4; };
std::cout << "func1: " << func1() << '\n';
// like all callable objects, closures can be captured in std::function
// (this may incur unnecessary overhead)
std::function<int(int)> func2 = [](int i) { return i + 4; };
std::cout << "func2: " << func2(6) << '\n';
constexpr int fib_max {8};
std::cout << "Emulate `recursive lambda` calls:\nFibonacci numbers: ";
auto nth_fibonacci = [](int n)
{
std::function<int(int, int, int)> fib = [&](int n, int a, int b)
{
return n ? fib(n - 1, a + b, a) : b;
};
return fib(n, 0, 1);
};
for (int i{1}; i <= fib_max; ++i)
{
std::cout << nth_fibonacci(i) << (i < fib_max ? ", " : "\n");
}
std::cout << "Alternative approach to lambda recursion:\nFibonacci numbers: ";
auto nth_fibonacci2 = [](auto self, int n, int a = 0, int b = 1) -> int
{
return n ? self(self, n - 1, a + b, a) : b;
};
for (int i{1}; i <= fib_max; ++i)
{
std::cout << nth_fibonacci2(nth_fibonacci2, i) << (i < fib_max ? ", " : "\n");
}
#ifdef __cpp_explicit_this_parameter
std::cout << "C++23 approach to lambda recursion:\n";
auto nth_fibonacci3 = [](this auto self, int n, int a = 0, int b = 1)
{
return n ? self(n - 1, a + b, a) : b;
};
for (int i{1}; i <= fib_max; ++i)
{
std::cout << nth_fibonacci3(i) << (i < fib_max ? ", " : "\n");
}
#endif
}
Possible output:
c: 5 6 7
func1: 10
func2: 10
Emulate `recursive lambda` calls:
Fibonacci numbers: 0, 1, 1, 2, 3, 5, 8, 13
Alternative approach to lambda recursion:
Fibonacci numbers: 0, 1, 1, 2, 3, 5, 8, 13
参考资料
Lambda expressions: https://en.cppreference.com/w/cpp/language/lambda
Lambda expressions: https://zh.cppreference.com/w/cpp/language/lambda