介绍

Lambda 表达式(Lambda Expression),在 C++11 中被引入,是一种可以就地定义、没有名字、能捕获外部变量的匿名函数对象(闭包)。

语法

这是一个完整的 Lambda 表达式:

1
[capture] (params) mutable exception attribute -> ret  { body }
  1. 捕获列表 [capture]:定义 Lambda 可以访问外部作用域的哪些变量,以及如何访问(按值还是按引用)。
  2. 参数列表 (params)(可选):与普通函数一致。
  3. 说明符 mutable(可选):默认情况下,按值捕获的变量在内部是 const 的,加上 mutable 允许修改这些副本。
  4. 异常声明 noexcept(可选):指定是否抛出异常。
  5. 返回类型 -> type(可选):通过尾置返回类型指定。
  6. 函数体 {}:函数具体的实现逻辑。

Lambda 表达式是以类(Class)形式定义,我们以一个最复杂的形式介绍:

1
2
3
4
5
int x = 10;
auto lambda = [x, &y](int a) mutable noexcept [[maybe_unused]] -> int {
x += a;
return x + y;
};

其 Class 展开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class __lambda_unique_id {
private:
int x;
int& y;

public:
__lambda_unique_id(int _x, int& _y) : x(_x), y(_y) {}

auto operator()(int a) mutable noexcept [[maybe_unused]] -> int {
x += a;
return x + y;
}

__lambda_unique_id(const __lambda_unique_id&) = default;
__lambda_unique_id& operator=(const __lambda_unique_id&) = delete;
};

捕获模式

Lambda 中的捕获拥有多种模式。

捕获形式说明
[]不捕获。Lambda 只能访问全局变量或静态变量。
[x]按值捕获 x。内部拥有 x 的副本,修改副本不影响外部。
[&x]按引用捕获 x。内部修改直接影响外部变量。
[=]隐式按值捕获。捕获函数体内用到的所有外部变量。
[&]隐式按引用捕获。捕获函数体内用到的所有外部变量。
[=, &x]默认按值捕获,但 x 必须按引用捕获。
[this]捕获当前类的指针,允许 Lambda 访问类的成员变量和成员函数。

generalized capture 带初始化的捕获

从 C++14 起,捕获列表 [capture] 支持自定义变量,但必须拥有初值,加上 mutable 后可变,生命周期跟随 Lambda。

也可以通过引用的方式定义一个变量,用于给外部变量一个别名。

参数列表

从 C++14 起,支持用 auto 声明参数,会生成 [[#泛型 Lambda]] 表达式。

如果不将参数传递给 lambda,并且其声明不包含 mutable,且没有后置返回值类型,则可以省略空括号.

返回类型

通常编译器可以自动推导返回类型,注意当 Lambda 中多个返回类型不一样且未指定返回类型,会产生编译错误。

泛型 Lambda

使用 auto 声明参数类型是,会构造泛型 Lambda。

1
auto add = [](auto a, auto b) { return a + b; };

其 Class 展开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class __lambda_unique_id {
public:
template<typename T1, typename T2>
auto operator()(T1 a, T2 b) const {
return a + b;
}

template<typename T1, typename T2>
static auto __invoke(T1 a, T2 b) {
return a + b;
}

template<typename T1, typename T2>
using fptr_t = decltype(add(std::declval<T1>(), std::declval<T2>())) (*)(T1, T2);

template<typename T1, typename T2>
operator fptr_t<T1, T2>() const {
return &__invoke;
}

__lambda_unique_id() = default;
__lambda_unique_id(const __lambda_unique_id&) = default;
};
__lambda_unique_id add = __lambda_unique_id{};

额,看不懂没关系,只用注意到它是以 template<typename T1, typename T2> 模版形式定义就可以了。

Lambda 的递归

在 C++ 中,Lambda 的递归比普通函数要复杂一些。核心矛盾在于:Lambda 在定义完成之前,它的名字(变量名)在函数体内是不可见的。

比如对于一个 错误实现

1
auto fib = [](int n) { if(n<=1) return n; return fib(n-1); }

因为推导 fib 的类型需要知道它的返回类型,而确定返回类型又需要调用 fib,陷入了死循环。

所以接下来介绍主要的三种实现方式。

std::function

注意会有额外性能开销

1
std::function<int(int)> fib = [&](int n) { return (n <= 1) ? n : fib(n - 1) + fib(n - 2); };

这是最常用的方法。通过显式指定 std::function 类型,提前确定了 Lambda 的签名,使得内部可以识别这个名称。

注意不建议使用该方式,存在类型擦除和动态内存分配的开销,且无法进行内联优化,性能略低。

泛型 Lambda 自传递

1
2
3
4
5
6
7
auto fib = [](auto self, int n) -> int {
if (n <= 1) return n;
return self(self, n - 1) + self(self, n - 2);
};

// 调用方式
int result = fib(fib, 10);

其 Class 展开:

1
2
3
4
5
6
7
8
9
class __lambda_fib {
public:
template<typename T>
auto operator()(T self, int n) const -> int {
if (n <= 1) return n;
return self(self, n - 1) + self(self, n - 2);
// 这里的 self 实际上就是传入的 __lambda_fib 实例
}
};

零开销。编译器可以完全内联,性能等同于普通递归函数。

只是语法可能略显奇怪。

使用辅助函数 std::visit 或自定义 y_combinator

为了解决上一个方法 多传一个参数 的尴尬,可以写一个辅助包装类(通常称为 y_combinator)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<class F>
struct y_combinator {
F f;
template<class... Args>
auto operator()(Args&&... args) const {
return f(*this, std::forward<Args>(args)...);
}
};

auto fib = y_combinator{[](auto self, int n) -> int {
return (n <= 1) ? n : self(n - 1) + self(n - 2);
}};

int res = fib(10);

优点在于调用语法非常亲切。

缺点在于比较麻烦。

C++23

C++23 引入了 “Deducing this” 语法,允许 Lambda 直接显式访问自身,彻底解决了递归问题。

1
2
3
4
5
auto fib = [](this auto&& self, int n) -> int {
return (n <= 1) ? n : self(n - 1) + self(n - 2);
};

int res = fib(10);