C++ 踩坑: lambda 表达式
废话不多说, lambda 表达式应该是 C++11 之后的一个非常重要且实用的特性.
lambda 基础
C++ 参考文档列出了几种 Lambda 的形式 Lambda expressions
语法
[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, 显式提供模板参数列表
- 同 3)
参数说明
- captures: 捕获参数列表, 逗号分隔, 可以以默认捕获开头 (注: 空列表, &, =).
- Lambda 表达式可以直接使用以下类型的变量, 而无须捕获:
- 非局部变量, 或者具有
static, 或者 线程局部存储thread local storage duration(变量无法被捕获) 的变量, 或者 - 已经由常量表达式 (
constant expression) 初始化的引用.
- 非局部变量, 或者具有
- Lambda 表达式读取以下类型的变量, 而无需捕获:
- 具有
const non-volatile整形或者枚举值, 或者已经被常值表达式初始化的变量, 或者 - 是
constexpr并且非 mutable成员的变量.
- 具有
- Lambda 表达式可以直接使用以下类型的变量, 而无须捕获:
- tparams: 非空的模板参数列表, 一般为提供泛型 lambda 的模板参数名.
- params: 参数列表, 和普通函数的参数列表一样.
- specs: 按
specifiers,expection,attr,trailing-return-type的顺序组成, 每种标识都是可选的.specifiers: 可选的修饰符 (specifiers) 序列. 如果没有提供, 被拷贝捕获(注: 按值捕获) 的变量将是const的. 以下修饰符在序列中最多允许出现一次.mutable: 允许按值捕获的变量在函数体内被修改, 并且可以调用non-const的成员函数.constexpr(since C++17): 显式指定函数调用符或者任一给定的模板特化的调用符是一个constexpr函数. 若无此修饰符, 当函数调用符或者给定的模板特化的调用符满足所有constexpr函数的前提时, 它们也将是constexpr的.consteval(since C++20): 指定函数调用符或者给定的模板特化的调用符是immediate function.constexpr和consteval不能同时使用.
expection: 为闭包类型的operator()提供动态异常说明 (dynamic exception specification) 或者无异常修饰符.attr: 为函数调用符或者闭包类型的调用符模板提供属性指示符. 任何被指定的属性不能和函数调用符或者调用符模板本身相关. (例如, 不能使用[[noreturn]]属性).trailing-return-type:-> ret, 其中ret指定返回类型. 如果没有trailing-return-type, 那么闭包的operator()的返回类型则会从return语句推导, 类似于函数返回一个以auto声明的类型.
requires: (since C++20) 给闭包类型的operator()添加限定符.body: 函数体.
MSDN 有一张图也表达的比较清晰
- captures,
- parameters,
- mutable,
- expection,
- trailing-return-type,
- lambda body
lambda 捕获
captures: 捕获参数列表, 逗号分隔, 可以以默认捕获开头. 被捕获的外部变量可以在 lambda 函数体内被访问. 默认捕获包括:
&: 隐式地通过引用捕获已被用到的自动变量=: 隐式地通过拷贝捕获 (值捕获) 已被用到的自动变量
如果没有默认捕获, 那么当前对象(*this)将被隐式捕获. 如果被隐式捕获了, 它总是被引用捕获的, 即使默认捕获是 = (从 C++20 开始, 当默认捕获是 = 时, 隐式捕获 *this 被废弃).
captures 中的单个捕获语法:
identifier: 拷贝捕获.identifier ...: 拷贝捕获包扩展(parameter pack).identifier initializer: 带初始化器的拷贝捕获. (since C++14)& identifier: 引用捕获.& identifier...: 引用捕获包扩展& identifier initializer: 带初始化器的引用捕获. (since C++14)this: 引用捕获当前对象.*this: 拷贝捕获当前对象. (since C++17)... identifier initilizer: 带初始化器的拷贝捕获包扩展. (since C++20)& ... identifier initilizer: 带初始化器的引用捕获包扩展. (since C++20)
如果默认捕获是 &, 那么后续的捕获不能再以 & 捕获:
struct S2 { void f(int i); };
void S2::f(int i)
{
[&]{}; // OK: by-reference capture default
[&, i]{}; // OK: by-reference capture, except i is captured by copy
[&, &i] {}; // Error: by-reference capture when by-reference is the default
[&, this] {}; // OK, equivalent to [&]
[&, this, i]{}; // OK, equivalent to [&, i]
}
如果默认捕获是 =, 那么后续的捕获必须以 & 捕获, 或者 *this (since C++17), 或者 this (since C++20):
struct S2 { void f(int i); };
void S2::f(int i)
{
[=]{}; // OK: by-copy capture default
[=, &i]{}; // OK: by-copy capture, except i is captured by reference
[=, *this]{}; // until C++17: Error: invalid syntax
// since C++17: OK: captures the enclosing S2 by copy
[=, this] {}; // until C++20: Error: this when = is the default
// since C++20: OK, same as [=]
}
任一捕获只可能出现一次, 且名称必须和其他参数名不同:
struct S2 { void f(int i); };
void S2::f(int i)
{
[i, i] {}; // Error: i repeated
[this, *this] {}; // Error: "this" repeated (C++17)
[i] (int i) {}; // Error: parameter and capture have the same name
}
示例
下面两个示例均在 C++20 标准下编译.
捕获局部变量
class S {
public:
void f() {
int x = 0;
// OK, copy capture
auto l1 = [x=x] { std::cout << "x: " << x << std::endl; };
x = 1;
l1(); // x: 0
// OK, copy capture
auto l2 = [x] { std::cout << "x: " << x << std::endl; };
x = 2;
l2(); // x: 1
// OK, implicitly capture by value
auto l3 = [=] { std::cout << "x: " << x << std::endl; };
x = 3;
l3(); // x: 2
// OK, reference capture
auto l4 = [&x=x] { std::cout << "x: " << x << std::endl; };
x = 4;
l4(); // x: 4
// OK, reference capture
auto l5 = [&x] { std::cout << "x: " << x << std::endl; };
x = 5;
l5(); // x: 5
// OK, implicitly capture by reference
auto l6 = [&] { std::cout << "x: " << x << std::endl; };
x = 6;
l6(); // x: 6
}
};
- 拷贝捕获
x. (=左侧为显式声明的别名, 可以为其他名). - 拷贝捕获
x. - 拷贝捕获, 隐式.
- 引用捕获
x. (=左侧为显式声明的别名, 可以为其他名). - 引用捕获
x. - 引用捕获, 隐式.
捕获成员变量
class S {
public:
int x = 0;
void f() {
// OK, copy capture
auto l1 = [x=x] { std::cout << "x: " << x << std::endl; };
x = 1;
l1(); // x: 0
// OK, reference capture
auto l2 = [&x=x] { std::cout << "x: " << x << std::endl; };
x = 2;
l2(); // x: 2
// OK, copy capture this pointer
auto l3 = [this] { std::cout << "x: " << x << std::endl; };
x = 3;
l3(); // x: 3
// OK, copy capture this pointer, implicit capture of '**this**' via ' **[=]** ' is deprecated in C++20
auto l4 = [=] { std::cout << "x: " << x << std::endl; };
x = 4;
l4(); // x: 4
// OK, capture this by reference
auto l5 = [&] { std::cout << "x: " << x << std::endl; };
x = 5;
l5(); // x: 5
// OK, capture this by value
auto l6 = [*this] { std::cout << "x: " << x << std::endl; };
x = 6;
l6(); // x: 5
// error: capture of non-variable 'S::x'
// auto l = [x] { std::cout << "x: " << x << std::endl; };
}
};
- 拷贝捕获
x,=左侧为显式声明的别名, 可以为其他名. - 引用捕获
x, 同上. - 拷贝捕获
this指针, 相当于隐式引用捕获x. - 同上. C++20 已经废弃.
- 同上.
- 拷贝捕获
*this, 包括拷贝其中的成员. - 成员变量无法通过显式捕获列表来捕获, 除非有通过初始化器 (默认/显式初始化器, 参考 1 ~ 6).
lambda 与 template
C++14开始, 我们可以通过 auto 接受模板参数, 来定义泛型 lambda. 比如:
auto lambda = [](auto i) { std::cout << typeid(i).name() << std::endl; };
lambda(1.1);
lambda(1);
lambda("abc");
lambda(std::vector<int>{1, 2});
等效于
template<typename T>
void generic_function(T x) { std::cout << typeid(x).name() << std::endl; }
但是 auto 的声明方式限制了我们在 lambda 表达式内部想使用实际类型的需求. 比如, 我们仅需要处理传入的 vector 类型, 就很难进行限制. 如果调用出错, 错误信息也不太能定位到原始的问题. 比如,
auto lambda = [](auto i) {
std::cout << typeid(i).name() << std::endl;
std::cout << i.size() << std::endl;
};
lambda(std::vector<int>{1, 2});
// error: request for member 'size' in 'i', which is of non-class type 'const char*'
lambda("abc");
参考模板, C++20 使得我们可以定义模板 lambda 表达式. 如果类型不匹配, 编译器也能清晰地输出错误信息. 比如,
auto lambda = []<typename T>(std::vector<T> i) {
std::cout << typeid(i).name() << std::endl;
std::cout << i.size() << std::endl;
};
lambda(std::vector<int>{1, 2});
// error: no match for call to '(main()::<lambda(std::vector<T>)>) (const char [4])'
lambda("abc");
这种 lambda 等价于
template<typename T>
void generic_function(std::vector<T> x) { std::cout << typeid(x).name() << std::endl; }
lambda 与 functor
functor 译名为仿函数,但仿函数不是函数,而是定义了 operator() 方法的函数对象。而 lambda 则是匿名函数。Functor 可以维护多个内部状态变量。两者有很多类似之处,lambda 通常可以转换为 functor 表示。
无捕获
// functor
struct functor
{
auto operator()(auto x) { return x * x; }
};
// lambda
auto lambda = [](auto x) { return x * x; };
运行代码, 可以看到 functor 和 lambda 编译后的结果是一模一样的。
auto functor::operator()<double>(double):
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
movsd QWORD PTR [rbp-16], xmm0
movsd xmm0, QWORD PTR [rbp-16]
mulsd xmm0, xmm0
movq rax, xmm0
movq xmm0, rax
pop rbp
ret
auto main::{lambda(auto:1)#1}::operator()<double>(double) const:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
movsd QWORD PTR [rbp-16], xmm0
movsd xmm0, QWORD PTR [rbp-16]
mulsd xmm0, xmm0
movq rax, xmm0
movq xmm0, rax
pop rbp
ret
拷贝捕获
注意,lambda 拷贝捕获的值为 const 类型,无法在函数体内部被修改。若想修改值,比如进行累加,则需要关键字 mutable 修饰 (见后续)。
// functor const
template <typename T>
struct functor
{
const T i_;
functor(T i) : i_(i) { }
auto operator()(auto x) { return i_ + x; }
};
// lambda const
auto lambda = [i](auto x) { return i + x; };
// functor non-const
template <typename T>
struct functor
{
const T i_;
functor(T i) : i_(i) { }
auto operator()(auto x) { return i_ + x; }
};
// lambda mutable
auto lambda = [i](auto x) mutable { return i + x; };
引用捕获
// functor
template <typename T>
struct functor
{
T &i_;
functor(T &i) : i_(i) { }
auto operator()(auto x) { return i_ + x; }
};
// lambda
auto lambda = [&i](auto x) { return i + x; };
lambda 与 mutable
一般来说, lambda 表达式拷贝捕获的变量是 const 的. 如下, lambda_const 将产生编译错误, 因为拷贝捕获的 n 是一个只读变量. 因此需要通过修饰符 mutable 来修饰. 这样, 在 lambda 函数体中允许修改 n. 但由于是拷贝捕获, 因此在退出 lambda 后, n 依然是 0, 而由于 m 是引用捕获, 因此其值被修改为 3.
int main()
{
int m = 0;
int n = 0;
// error: increment of read-only variable 'n'
// auto lambda_const = [&, n] (int a) { m = ++n + a; };
// lambda_const();
auto lambda_mutable = [&, n] (int a) mutable { m = ++n + a; };
lambda_mutable(2);
std::cout << m << ", " << n << std::endl; // 3, 0
}
lambda 与 constexpr
从 C++17 开始, lambda 表达式可以被声明为 constexpr (这里, 我们可以通过 static_assert 来验证是否为编译期的常量表达式).
constexpr int increment(int n) { return [n]{ return n + 1; }(); }
static_assert(3 == increment(2));
int main()
{
constexpr int y = 2;
auto answer = [y](auto x) constexpr { return y + x; };
static_assert(5 == answer(3));
}
对于满足 constexpr specifier (since C++11) 的函数, 都将隐式声明为 constexpr. 在这个例子中 lambda 表达式的 constexpr 不是必须的.
/*constexpr*/ auto lambda = [](auto x) { return x + x; };
constexpr auto si = lambda(1);
static_assert(2 == si);
lambda 与 this, *this
访问数据成员
在如何捕获成员变量的例子中, 曾提到过通过捕获 this 或者 *this 来隐式访问到成员变量. 其中一种方式是通过捕获 this 指针来访问成员. 然而, 捕获 this (实际上是一个指针), 会造成潜在的隐患.
另一种方式是通过隐式捕获 this 对象. 不过, 隐式捕获这种方式在 C++20 中被废弃, 编译器将给出警告 **warning:** implicit capture of '**this**' via ' **[=]** ' is deprecated in C++20 [**-Wdeprecated**]. 假如实在想捕获在 this, 那么可以使用 [=, this] 方式.
struct Bar
{
int i_ = 2;
// C++11/C++14/C++17/C++20
void CaptureThisPointerExplicitly() {
auto lambda = [this]() { std::cout << i_ << std::endl; };
lambda();
}
// C++20: **warning:** implicit capture of '**this**' via ' **[=]** ' is deprecated in C++20 [**-Wdeprecated**]
// C++11/C++14/C++17
void CaptureThisPointerImplicitlyUntilCpp20() {
auto lambda = [=]() { std::cout << i_ << std::endl; };
lambda();
}
// C++11/C++14/C++17/C++20
void CaptureThisPointerExplicitly2() {
auto lambda = [=, this]() { std::cout << i_ << std::endl; };
lambda();
}
};
那么该如何通过拷贝捕获 *this 对象呢? 在 C++17/C++20 中, 我们可以直接捕获 *this.
struct Bar
{
int i_ = 2;
// C++14: **warning:** ' ***this**' capture only available with '**-std=c++17**' or '**-std=gnu++17**' [**-Wc++17-extensions**]
// C++17/C++20
void CaptureThisValueExplicitlySinceCpp17() {
auto lambda = [*this]() { std::cout << i_ << std::endl; };
lambda();
}
};
上述方式, 在 C++14 中, 编译器将给出警告 warning: '*this' capture only available with '-std=c++17' or '-std=gnu++17'. 除了隐式地拷贝捕获方法外, 我们还可以通过显式地初始化来捕获当前 *this 对象.
struct Bar
{
int i_ = 2;
// C++14/C++17/C++20
void CaptureThisValueExplicitlySinceCpp14() {
auto lambda = [self = *this]() { std::cout << self.i_ << std::endl; };
lambda();
}
};
访问方法成员
拷贝捕获对象指针, 对于指针指向的值并没有常量的限制, 因此, 允许修改成员数据.
struct Bar
{
int i_ = 2;
// C++11/C++14/C++17/C++20
void CaptureThisPointerExplicitly() {
auto lambda = [this]() { Print(); };
lambda();
}
// C++11/C++14/C++17/C++20
void CaptureThisPointerExplicitly2() {
auto lambda = [=, this]() { Print(); };
lambda();
}
// C++11/C++14/C++17
void CaptureThisPointerImplicitlyUnitilCpp17() {
auto lambda = [=]() { Print(); };
lambda();
}
void Print() { std::cout << ++i_ << std::endl; }
};
int main()
{
Bar bar;
bar.CaptureThisPointerExplicitly(); // 3
bar.CaptureThisPointerExplicitly2(); // 4
bar.CaptureThisPointerImplicitly(); // 5
}
拷贝捕获指针对象, 由于拷贝捕获的类型默认是 const, 因此无法修改数据成员, 且 lambda 表达式访问的方法也必须是 const 方法.
struct Bar
{
int i_ = 2;
// C++17/C++20
void CaptureThisValueExplicitlySinceCpp17() {
auto lambda = [*this]() { Print(); };
lambda();
}
// C++14/C++17/C++20
void CaptureThisValueExplicitlySinceCpp14() {
auto lambda = [self = *this]() { self.Print(); };
lambda();
}
void Print() const { std::cout << i_ << std::endl; }
};
int main()
{
Bar bar;
bar.CaptureThisValueExplicitlySinceCpp17(); // 2
bar.CaptureThisValueExplicitlySinceCpp14(); // 2
}
lambda 递归
既然 lambda 是一类函数, 那么能否通过 lambda 来实现递归呢? 答案是肯定的. 以递归计算阶乘为例. 普通函数版本, 我们能很快写出,
int factorial_function(int n) {
return n > 1 ? n * factorial_function(n - 1) : 1;
}
那是否 lambda 表达式是否也可以呢?
int main()
{
auto factorial = [](int n) {
return n > 1 ? n * factorial(n - 1) : 1;
};
factorial(5);
}
很不幸, 上述例子无法通过编译, 即使在 C++20 标准下. 因为编译时需要明确 factorial 类型, 但 lambda 的表达式又依赖于 factorial, 导致类型无法推导出来, 产生编译错误.
error: use of 'factorial' before deduction of 'auto'
那么, 如何正确使用 lambda 来实现递归呢? 这里提供两种方式.
使用 std::function
通过将 lambda 定义为一个 std::function 对象, 然后对其进行捕获, 那么
int main()
{
std::function<int(int)> factorial = [&factorial](int n) {
return n > 1 ? n * factorial(n - 1) : 1;
};
assert(120 == factorial(5));
}
将 lambda 作为自身入参
这种方法 (从 C++14 开始) 将使我们无需依赖于 std::function. 需要注意的是, 必须声明尾置返回类型, 否则将无法通过编译.
int main() {
// Since C++14
auto factorial = [](int n, auto& impl) -> int {
return n > 1 ? n * impl(n - 1, impl) : 1;
};
assert(120 == factorial(5, factorial));
}
lambda 作为默认参数
如果 lambda 表达式作为默认参数, 那么它将不能显式或隐式地捕获任何变量, 除非所有捕获均具有满足初始化器, 满足作为表达式出现在默认参数中的限制 (since C++14).
void f2()
{
int i = 1;
void g1(int = ([i]{ return i; })()); // error: captures something
void g2(int = ([i]{ return 0; })()); // error: captures something
void g3(int = ([=]{ return i; })()); // error: captures something
void g4(int = ([=]{ return 0; })()); // OK: capture-less
void g5(int = ([]{ return sizeof i; })()); // OK: capture-less
// C++14
void g6(int = ([x = 1] { return x; }))(); // OK: 1 can appear in a default argument
void g7(int = ([x = i] { return x; }))(); // error: i cannot appear in a default argument
}
g6: 可以理解为,1可以作为默认参数, 相当于void g6(int = 1), 所以g6正确. 甚至这样可以:void g6(int = ([x = 1, y = 2] { return x + y; })());.g7: 而i无法作为默认参数出现,void g7(int = i)编译错误.