代码存储位置:shanchuann/Modern_CPP

lambda 表达式是 C++11 引入的匿名函数,是一个右值表达式,其类型为唯一的未命名非联合聚合类型,称为闭包类型(closure type),它被声明在包含 lambda 表达式的最小块作用域、类作用域或命名空间作用域中(用于 ADL)。它可以在需要函数的地方直接定义,无需单独声明,极大简化了代码编写。

1
2
3
4
5
6
7
8
9
//lambda 表达式是 C++11 引入的**匿名函数**,属于*无名的非联合非聚类*类型
struct X :decltype([] {}) { // 继承自 lambda 表达式的类型
void operator()() { cout << "X::operator()" << endl; }
};
int main() {
X x;
x(); // 调用 X::operator() //输出:X::operator()
return 0;
}

这是一个有 operator() 的类,在默认情况下 operator() 函数是一个 const 函数,他的 this 指针被 const 修饰。

1
2
3
4
5
6
7
8
9
10
#include <iostream>
using namespace std;

int main() {
auto p = [num = 0] {

};
return 0;
cout << sizeof p;
}

经过 C++ Insights 将源代码转化为编译器的推导结果后

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
#include <iostream>
using namespace std;

int main()
{

class __lambda_5_11
{
public:
inline /*constexpr */ void operator()() const
{
}

private:
int num;

public:
__lambda_5_11(const int & _num)
: num{_num}
{}

};

__lambda_5_11 p = __lambda_5_11{0};
return 0;
std::cout.operator<<(sizeof(p));
}

可以很明确的发现默认情况下 operator() 函数是一个 const 函数。

总而言之,lambda 表达式的底层是一个编译器生成的匿名类(闭包类型),该类会重载 operator()(函数调用运算符)。默认情况下,这个 operator()const 成员函数,而按值捕获的外部变量会被存储为该类的成员变量

在 C++ 中,const 成员函数内部不能修改非 mutable 的成员变量。因此:

  • 按值捕获的变量在 lambda 内部默认是 “只读” 的(因为闭包的 operator() 是 const,且成员变量非 mutable)。
  • 若要在 lambda 内部修改按值捕获的副本,必须用 mutable 关键字取消 operator() 的 const 属性(C++23 前需要在 [] 后添加 () 使得编译器不报错)。
1
2
//添加`()mutable`前
1>D:\cpp language\现代C++\lambda.cpp(5,22): error C3491: “num”: 无法在非可变 lambda 中修改通过复制捕获

添加后:

1
2
3
4
5
6
7
8
9
int main() {
auto p = [num = 0]()mutable { num = 0; };
return 0;
}
public:
inline /*constexpr */ void operator()()
{
num = 0;
}

const 消失。

基本语法

lambda 表达式的完整语法格式如下:

1
[捕获列表](参数列表) mutable noexcept/throw() -> 返回类型 { 函数体 }

各部分的含义:

  • 捕获列表:指定 lambda 表达式能访问的外部变量(局部作用域的变量),是 lambda 与外部环境交互的关键(详见下文)。
  • 参数列表:与普通函数的参数列表一致,可省略(无参数时)。
  • mutable:可选关键字,允许修改按值捕获的变量(默认按值捕获的变量是 const,不可修改)。
  • noexcept/throw ():可选,指定函数是否抛出异常(C++11 起)。
  • -> 返回类型:可选,指定函数返回值类型。若函数体只有一个 return 语句,编译器可自动推导,此时可省略。
  • 函数体:lambda 的具体逻辑,与普通函数体一致。

捕获列表

捕获列表用于控制 lambda 对外部局部变量的访问权限,常见形式如下:

捕获方式 含义
[] 空捕获,不访问任何外部变量
[=] 按值捕获所有外部变量(副本)
[&] 按引用捕获所有外部变量(原变量)
[x] 仅按值捕获变量 x(副本)
[&y] 仅按引用捕获变量 y(原变量)
[=, &x] 除 x 按引用捕获外,其余按值捕获
[&, x] 除 x 按值捕获外,其余按引用捕获
[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
#include <iostream>
using namespace std;

int main() {
int a = 10, b = 20;

// 1. 空捕获:不能访问a、b
auto func1 = []() { cout << "空捕获" << endl; };
func1();

// 2. 按值捕获a,按引用捕获b
auto func2 = [a, &b]() {
// a是副本,默认不可修改(加mutable可改,但不影响外部)
// a++; // 错误:按值捕获的变量默认是const
b++; // 引用捕获,可修改外部变量
cout << "a=" << a << ", b=" << b << endl; // a=10, b=21
};
func2();
cout << "外部b=" << b << endl; // 21(被修改)

// 3. 按值捕获所有,用mutable允许修改副本
auto func3 = [=]() mutable {
a++; // 允许修改副本(不影响外部a)
cout << "副本a=" << a << endl; // 11
};
func3();
cout << "外部a=" << a << endl; // 10(未变)

return 0;
}

用户定义转换函数仅在 lambda 表达式没有 捕获列表 且没有显式对象参数时才定义(C++23 起)。它是闭包对象的公共、constexpr、(C++17 起)非虚、非显式、const noexcept 成员函数。

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
//普通lambda的转换函数
int main() {
int(*p)(int) = [](int n) { return n; };
return 0;
}
//经过**C++ Insights**将源代码转化为编译器的推导结果后
int main()
{

class __lambda_5_17
{
public:
inline /*constexpr */ int operator()(int n) const
{
return n;
}

using retType_5_17 = int (*)(int);
inline constexpr operator retType_5_17 () const noexcept
{
return __invoke;
}

private:
static inline /*constexpr */ int __invoke(int n)
{
return __lambda_5_17{}.operator()(n);
}


public:
// /*constexpr */ __lambda_5_17() = default;

};

using FuncPtr_5 = int (*)(int);
FuncPtr_5 p = __lambda_5_17{}.operator __lambda_5_17::retType_5_17();
return 0;
}

参数与返回类型

  • 参数列表:与普通函数一致,支持默认参数(C++14 起),甚至泛型参数(auto,C++14 起,称为 “泛型 lambda”)。
  • 返回类型:若函数体只有一个 return,可省略 -> 返回类型,编译器自动推导。
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
#include <iostream>
using namespace std;

int main() {
// 无参数,无返回值
auto print = []() { cout << "Hello Lambda" << endl; };
print();

// 有参数,自动推导返回类型(int)
auto add = [](int x, int y) { return x + y; };
cout << add(2, 3) << endl; // 5

// 显式指定返回类型(当返回类型复杂时)
auto divide = [](double x, double y) -> double {
if (y == 0) return 0;
return x / y;
};
cout << divide(10, 3) << endl; // 3.333...

// 泛型lambda(C++14,参数类型自动推导)
auto sum = [](auto a, auto b) { return a + b; };
cout << sum(2, 3) << endl; // 5(int)
cout << sum(2.5, 3.5) << endl; // 6(double)

return 0;
}

泛型 lambda

泛型 lambda(Generic Lambda)是 C++14 引入的重要特性,它允许 lambda 表达式的参数使用 auto 关键字声明,从而让 lambda 能够接受任意类型的参数,实现类似模板函数的通用逻辑。

泛型 lambda 的定义与语法

泛型 lambda 的核心是 ** 参数列表中使用 auto**,语法格式如下:

1
[捕获列表](auto 参数1, auto 参数2, ...) -> 返回类型 { 函数体 }

与普通 lambda 相比,仅参数类型用 auto 替代了具体类型(如 intdouble 等)。编译器会根据传入的实际参数自动推导类型,实现 “一次定义,多类型适用”。

1
2
3
4
5
6
7
int main() {
auto p = [](auto n) {cout << n << endl; };
p(10); // 输出 10
p(3.14); // 输出 3.14
p("hello"); // 输出 hello
return 0;
}

当然,写成模板形参同样可行

1
auto p = []<typename T>(T n) { cout << n <<endl; };

输出:

1
2
3
10
3.14
hello

常量表达式

如果 operator () 满足 constexpr 函数的要求,则它总是 constexpr。如果 lambda 说明符中使用了关键字 constexpr,它也是 constexpr。

1
2
3
4
5
6
int main() {
auto p = [] { return 42; }; // auto p = []()constexpr { return 42; };
constexpr int n = p(); // p() 是常量表达式
cout << n << endl; // 输出 42
return 0;
}

隐式 constexproperator() 自动满足条件时

在 C++17 及以上标准中,若 lambda 的 operator() 满足 constexpr 函数的所有要求 ,则该 operator() 会被隐式声明为 constexpr,无需显式添加 constexpr 说明符。

constexpr 函数的核心要求(适用于 lambda 的 operator()):

  1. 函数体中不能包含汇编语句、goto、非 constexpr 函数调用(除非在未求值上下文中)等。
  2. 不能修改非 mutable 的非 constexpr 变量(对 lambda 而言,若按值捕获非 constexpr 变量,修改会违反此条)。
  3. 参数和返回类型必须是字面类型(literal type,如基本类型、constexpr 构造的类等)。
  4. C++17 中,lambda 不能有捕获(或仅捕获 constexpr 变量,C++20 放宽);C++20 允许捕获更多类型(如 constexpr 对象的引用、用 constexpr 初始化的按值捕获等)。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// C++17及以上
#include <iostream>
using namespace std;

int main() {
// 无捕获,函数体仅包含编译期可执行的操作(加法)
auto add = [](int a, int b) { return a + b; };

// 可在constexpr上下文中使用(编译期计算)
constexpr int sum1 = add(2, 3); // 合法,sum1=5(编译期计算)
static_assert(sum1 == 5);

// 运行时调用也合法
int a = 4, b = 5;
int sum2 = add(a, b); // 运行时计算,sum2=9
return 0;
}

这里的 add lambda 未显式声明 constexpr,但因其 operator() 满足所有 constexpr 要求(无捕获、操作可在编译期完成),故 operator() 被隐式视为 constexpr,可用于编译期计算。

显式 constexpr:强制 operator()constexpr

若在 lambda 中显式添加 constexpr 说明符(语法:constexpr [捕获列表](参数列表) { ... }),则编译器会强制 operator() 必须满足 constexpr 函数的要求。若不满足,编译会直接报错(即使隐式情况下可能不触发错误)。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// C++17及以上
auto multiply = constexpr [](int a, int b) { return a * b; };
// 显式声明constexpr,编译器会检查是否满足条件

constexpr int prod1 = multiply(3, 4); // 合法,编译期计算(12)

// 若lambda不满足constexpr要求,显式声明会报错:
auto invalid = constexpr [](int a) {
// 包含非constexpr操作(如修改非constexpr的外部变量)
int x = 0;
x++; // 看似简单,但C++17中lambda若有捕获外的状态修改,是否允许?
return a + x;
};
// 上述代码在C++17中编译错误:x的修改本身是允许的,但显式constexpr要求整个逻辑可在编译期执行,此处无问题?不,实际更复杂:x是局部变量,修改是允许的,但需确保整个函数体可constexpr执行。真正的错误可能来自其他场景,如捕获非constexpr变量。

// 更典型的错误:C++17中显式constexpr lambda捕获非constexpr变量
int y = 10; // 非constexpr变量
auto bad = constexpr [y]() { return y; }; // 编译错误(C++17):按值捕获非constexpr变量,违反constexpr要求

显式 constexpr 与隐式 constexpr 的区别

场景 隐式 constexpr(无 constexpr 说明符) 显式 constexpr(有 constexpr 说明符)
触发条件 自动判断:若 operator() 满足 constexpr 要求,则隐式为 constexpr 强制要求:operator() 必须满足 constexpr 要求,否则编译报错
用途 可在编译期 / 运行时调用(根据上下文自动适配) 明确告知编译器和开发者:此 lambda 设计为可在编译期执行,且必须满足条件
兼容性 若后续修改导致不满足 constexpr 要求,仅影响编译期调用的场景 若后续修改导致不满足 constexpr 要求,直接编译报错(无论是否用于编译期)