在另一篇关于可调用对象的笔记中已经对lambda表达式的语法本质和应用情景进行了整理,
这篇笔记主要是整理lambda表达式的语法细节,假定读者对lambda表达式已经有了基本的概念。
虽然早在C++11中就提出了lambda表达式,但是相关的语法细节始终在不断地发展和完善(C++实在是太复杂了!),
本文以C++20已经支持的语法为主,对于最新的C++23增加的语法不作讨论,例如Deducing This等内容。
基础
基本捕获
首先介绍两种隐式捕获符:
它们会自动地捕获在lambda表达式中所有实际被使用的局部变量,无需我们逐个列出被使用变量对应的名称。
这里存在一个问题:如果当前处于一个普通成员函数中,如何处理特殊的this/*this所代表的当前对象?见下文中的讨论。
在默认捕获符的基础上,我们可以进行一些微调,例如:
[=, &a, &b]:表示除了后面明确提到的这些变量按引用捕获,其它情况下默认按值捕获
[&, a, b]:表示除了后面明确提到的这些变量按值捕获,其它情况下默认按引用捕获
注意,在语法上不允许出现例如[=,a]或者[&,&a]的形式,附加的情况必须与前面默认捕获符不同。
但是实践发现,使用任何一种默认捕获符都会非常影响程序的可读性,因此我们通常不建议使用它们,
而是建议显式地列出所有需要捕获的局部变量,例如[a, &b, c],对它们的顺序并没有要求,值捕获和引用捕获可以交错出现。
补充:
- 即使没有捕获任何变量,也必须使用
[]开头,这是语法强制要求的;
- 对于捕获变量还有一个非常自然的要求:不允许与形参列表中的任何形参同名。
初始化捕获
有时我们的需求并不是捕获的并不是上下文中现有的局部变量,而是更复杂灵活的需求,例如
- 只需要现有局部变量的某个成员;
- 需要的是局部变量组成的表达式;
- 希望给捕获的局部变量起一个别名;
- …
这些需求可以通过C++14之后的初始化捕获实现,它允许我们在捕获列表中进行简单的变量初始化,例如
1 2 3 4 5 6 7
| int x = 4; auto y = [&r = x, x = x+1](){ r += 2; return x+2; }();
|
这里的lambda表达式在本质上可以理解为如下的匿名类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class __lambda_5_14 { public: inline int operator()() const { m_r = static_cast<int>(m_r + 2); return m_x + 2; }
private: int &m_r; int m_x;
public: __lambda_5_14(int &r, const int &x) : m_r{r}, m_x{x} {} };
auto y = __lambda_5_14{x, x + 1}();
|
一个更丰富的例子如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <iostream>
struct Point { double x; double y; };
int main() { int m = 3; int n = 4;
Point p = {1.0, 2.0}; auto f = [m1 = m, &n1 = n, px = p.x, &py = p.y]() { std::cout << m1 << " " << n1 << " " << px << " " << py << '\n'; };
f();
return 0; }
|
在本质上相当于如下的匿名类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class __lambda_14_14 { public: inline void operator()() const { std::cout << m_m1 << " " << m_n1 << " " << m_px << " " << m_py << '\n'; }
private: int m_m1; int &m_n1; double m_px; double &m_py;
public: __lambda_14_14(int &m1, int &n1, double &px, double &py) : m_m1{m1}, m_n1{n1}, m_px{px}, m_py{py} {} };
__lambda_14_14 f = __lambda_14_14{m, n, p.x, p.y};
|
初始化捕获有时候是非常必要的,例如将某些只允许移动的局部变量使用std::move传递到lambda表达式中
1 2 3 4 5 6 7 8 9
| class Widget { };
auto pw = std::make_unique<Widget>();
auto func = [pw = std::move(pw)] () { };
|
C++在定义一个变量时,通常需要先提供变量的类型(或者使用auto推断类型)
即使在if或者for语句中定义局部变量也无法省略类型名
1 2 3 4 5 6 7
| for (int i = 0; i < 10; ++i) { }
if (auto x = z; x > 0) { }
|
但是lambda表达式的初始化捕获却是一个例外,它确实起到了定义一个局部变量的效果,但是语法却不允许显式出现类型名。
1 2 3
| auto z = [x = 1]() { return x; };
|
无须捕获的变量
对于一些具有特殊性质的变量,无需显式出现在捕获列表中,在lambda表达式中就可以直接使用:
- 具有
constexpr等常量性质的变量,无需捕获就可以读取;
- 具有全局生命周期的变量,例如全局/静态变量等,对它们无需捕获就可以直接读写。
例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <iostream>
int m = 1;
const int c = 100;
int main() { static int n = 10;
auto f = []() { ++m; ++n; std::cout << "c = " << c; }; f();
std::cout << "m = " << m << '\n'; std::cout << "n = " << n << '\n';
return 0; }
|
返回值类型
在大多数情况下,我们不需要显式声明lambda表达式的返回值类型,因为编译器可以自动推断合适的返回值类型。
但是在某些复杂情况下我们仍然需要使用尾随类型方式来指明返回值类型,例如下面的代码无法通过编译,因为不同分支提供的返回值类型不一样,
编译器无法知晓真正的返回值类型
1 2 3 4 5 6 7
| auto myLambda = [](bool condition) { if (condition) { return 42; } else { return 3.14; } };
|
具体指明返回值的类型就可以通过编译,事实上我们可以有很多种选择,只要不同的返回值都可以向返回值类型进行转换即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| auto myLambda1 = [](bool condition) -> double { if (condition) { return 42; } else { return 3.14; } };
auto myLambda2 = [](bool condition) -> std::variant<int, double> { if (condition) { return 42; } else { return 3.14; } };
|
有意思的是,在这个例子中如果使用三目表达式,是可以直接通过编译的
1 2 3
| auto myLambda3 = [](bool condition) { return condition ? 42 : 3.14; };
|
修饰信息
lambda表达式可以在函数体之前添加一些起到修饰作用的说明符:(每个说明符最多允许出现一次,有的说明符相互冲突)
mutable:默认情况下编译器为lambda表达式生成的operator()方法是const版本的,但是加上这个就会让编译器生成非const的方法;
noexcept:让operator()方法具有noexcept性质,这通常是不需要的,因为编译器会在满足条件时自动为其加上noexcept性质;
constexpr:让operator()方法具有constexpr性质,这通常是不需要的,因为编译器会在满足条件时自动为其加上constexpr性质;
consteval:让operator()方法具有consteval性质,与constexpr不能同时使用。
例如
1 2 3 4 5 6
| auto lambda1 = [](int a, int b) constexpr { return a + b; }; auto lambda2 = [](int a, int b) consteval { return a + b; };
|
对于形参列表为空的lambda表达式,通常是可以直接省略()不写的,例如
1 2
| auto f1 = []{ return 1;}; auto f2 = [x=1]{ return 2;};
|
但是如果存在 constexpr、mutable、异常说明、属性或尾随返回类型这些内容,则()不允许省略。
把 lambda 传递给 STL 算法时,由于 STL 算法在实现时通常使用传递或拷贝方式接收 callable 对象,因此使用 mutable lambda 可能出现问题。
例如
1 2 3 4 5 6 7 8 9 10 11
| #include <vector> #include <algorithm> #include <iostream>
int main() { std::vector<int> v = {1, 2, 3}; int sum = 0;
std::for_each(v.begin(), v.end(), [sum](int x) mutable { sum += x; }); std::cout << sum << "\n"; }
|
进阶
this的捕获
对于在类的非静态成员函数体中定义的lambda表达式,如何处理特殊的this/*this所代表的当前对象是一个非常关键的问题,
对此C++提供了一些特殊的语法支持。
在只使用隐式捕获[=]和[&]的情况下,处理逻辑为:
[&]按照引用捕获当前对象自身,实质就是以值捕获的形式捕获this指针;
[=]在C++20之前与[&]的处理逻辑相同;在C++20之后则不会自动捕获当前对象自身。
示例代码如下(这里必须使用C++20之前的语法标准,在C++20之后,f1和g1将无法通过编译!)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <iostream>
struct Demo { void hello() { auto f1 = [=] { std::cout << s; };
auto f2 = [&] { std::cout << s; }; }
void hi() const { auto g1 = [=] { std::cout << s; };
auto g2 = [&] { std::cout << s; }; }
int s = 10; };
int main() { return 0; }
|
我们来理解一下这几个lambda表达式的语法实质:在非const方法中
1 2 3
| auto f1 = [=] { std::cout << s; };
auto f2 = [&] { std::cout << s; };
|
对应为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class __lambda_5_19 { public: inline void operator()() const { std::cout << m_this->s; }
__lambda_5_19(Demo *this_) : m_this{this_} {}
private: Demo *m_this; };
auto f1 = __lambda_5_19{this};
class __lambda_7_19 { public: inline void operator()() const { std::cout << m_this->s; }
__lambda_7_19(Demo *this_) : m_this{this_} {}
private: Demo *m_this; };
auto f2 = __lambda_7_19{this};
|
这表明两者生成的匿名类都在内部存储了Demo *类型的指针并指向当前对象,对外表现为按引用捕获当前对象。
在const方法中
1 2 3
| auto g1 = [=] { std::cout << s; };
auto g2 = [&] { std::cout << s; };
|
对应为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class __lambda_11_19 { public: inline void operator()() const { std::cout << m_this->s; }
__lambda_11_19(const Demo *this_) : m_this{this_} {}
private: const Demo *m_this; };
auto g1 = __lambda_11_19{this};
class __lambda_13_19 { public: inline void operator()() const { std::cout << m_this->s; }
__lambda_13_19(const Demo *this_) : m_this{this_} {}
private: const Demo *m_this; };
auto g2 = __lambda_13_19{this};
|
这表明两者生成的匿名类都在内部存储了const Demo *类型的指针并指向当前对象,对外表现为按const引用捕获当前对象。
与前文一样,在捕获变量时不建议使用隐式捕获的方式,在这个问题中尤其不建议。
在显式捕获时,下面是常见的几种语法和对应的效果:
[this],[ptr=this]:按值捕获this指针,相当于按引用捕获当前对象
[*this],[a=*this]:按值捕获当前对象,即完整创建一个当前对象的副本,这可能会产生很大的额外开销
[&p=this]:无法通过编译
[&p=*this]:按引用捕获当前对象
示例代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| #include <iostream>
struct Demo { void hello() { auto f1 = [this] { std::cout << 10; };
auto f2 = [*this] { std::cout << 20; };
auto f3 = [p = this] { std::cout << 30; };
auto f4 = [p = *this] { std::cout << 40; };
auto f6 = [&p = *this] { std::cout << 60; }; }
void hi() const { auto g1 = [this] { std::cout << 10; };
auto g2 = [*this] { std::cout << 20; };
auto g3 = [p = this] { std::cout << 30; };
auto g4 = [p = *this] { std::cout << 40; };
auto g6 = [&p = *this] { std::cout << 60; }; }
int s = 10; };
int main() { return 0; }
|
在非const方法中依次对应为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| class __lambda_5_19 { public: inline void operator()() const { std::cout.operator<<(10); }
__lambda_5_19(Demo *this_) : m_this{this_} {}
private: Demo *m_this; };
auto f1 = __lambda_5_19{this};
class __lambda_7_19 { public: inline void operator()() const { std::cout.operator<<(20); }
__lambda_7_19(const Demo &this_) : m_this{this_} {}
private: Demo m_this; };
auto f2 = __lambda_7_19{*this};
class __lambda_9_19 { public: inline void operator()() const { std::cout.operator<<(30); }
__lambda_9_19(Demo *p) : m_p{p} {}
private: Demo *m_p; };
auto f3 = __lambda_9_19{this};
class __lambda_11_19 { public: inline void operator()() const { std::cout.operator<<(40); }
__lambda_11_19(const Demo &p) : m_p{p} {}
private: Demo m_p; };
auto f4 = __lambda_11_19{*this};
class __lambda_15_19 { public: inline void operator()() const { std::cout.operator<<(60); }
__lambda_15_19(Demo &p) : m_p{p} {}
private: Demo &m_p; };
auto f6 = __lambda_15_19{*this};
|
在const方法中依次对应为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| class __lambda_19_19 { public: inline void operator()() const { std::cout.operator<<(10); }
__lambda_19_19(const Demo *this_) : m_this{this_} {}
private: const Demo *m_this; };
auto g1 = __lambda_19_19{this};
class __lambda_21_19 { public: inline void operator()() const { std::cout.operator<<(20); }
__lambda_21_19(const Demo &this_) : m_this{this_} {}
private: const Demo m_this; };
auto g2 = __lambda_21_19{*this};
class __lambda_23_19 { public: inline void operator()() const { std::cout.operator<<(30); }
__lambda_23_19(const Demo *p) : m_p{p} {}
private: const Demo *m_p; };
auto g3 = __lambda_23_19{this};
class __lambda_25_19 { public: inline void operator()() const { std::cout.operator<<(40); }
__lambda_25_19(const Demo &p) : m_p{p} {}
private: Demo m_p; };
auto g4 = __lambda_25_19{*this};
class __lambda_29_19 { public: inline void operator()() const { std::cout.operator<<(60); }
__lambda_29_19(const Demo &p) : m_p{p} {}
private: const Demo &m_p; };
auto g6 = __lambda_29_19{*this};
|
模板和泛型支持
在C++20中引入了模板lambda表达式,例如
1 2 3 4 5 6 7 8 9 10 11
| #include <iostream>
int main() { auto show = []<typename T>(T x) { std::cout << "show " << x << '\n'; };
show(42); show(3.14); show("hello");
return 0; }
|
在C++20中的lambda表达式中,我们可以直接用auto作为形参类型,它比模板写起来更加简单,例如
1
| auto f = [](auto a, auto b) { return a * b; };
|
编译器会将其改为模板lambda表达式并进一步处理。
调用即立刻执行
lambda表达式并不是必须要赋值给一个变量,也可以直接在定义后立刻调用执行,例如
1 2 3
| int main() { []{ std::print("Hello world!"); } (); }
|
更极端的例子如下
这两个语句都代表了一个捕获列表、形参列表和函数体均为空的lambda表达式,并且在定义之后立刻调用执行。
当然,这两个语句没有任何实际效果,只是一种语法炫技。
这种定义并立即执行的lambda表达式语句也是具有实际应用情景的,例如可以用来实现某些只需要执行一次的逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <iostream>
struct Demo { Demo() { static auto _ = [] { std::cout << "call once!"; return 0; }(); } };
int main() { Demo(); Demo();
return 0; }
|
对于某些非常复杂的全局/静态变量,直接用lambda表达式进行初始化也是部分人推荐的用法。
lambda表达式还可以用于给函数参数提供默认值,例如
1 2 3 4 5
| void func(int = [x = 10] { return x*x; }());
void func(int = 100);
|
为了适配作为参数默认值的角色,对此处的lambda表达式的捕获变量其实有一些额外要求,这里不作讨论。
补充:这种在最后加上 () 以立刻执行的写法在实践中非常容易出错,可以使用 std::invoke 显式调用 lambda 函数来替代。