Lambda 在各 C++ 版本的演进

C++11

虽然有了 auto 关键字,但是用起来还是需要 trailing return type 声明。

C++14

可以省略尾部声明(以下两种写法都是要 C++14 才能支持):

auto f()         { return 42; } // #1
auto f() -> auto { return 42; } // #2,相当于 #1

Note

例外:在 C++23 explicit object parameter 可用之前,构造递归 lambda 时需要显式声明返回值类型,否则无法成功推导(或者先用 function 模板类存储起来,类型也就能从 function 中推导)。

同时 C++14 还支持 generic lambda,即使用 auto 作为函数参数的类型。

decltype(auto) 也是 C++14 加入的。

C++20

支持给 lambda 声明模板参数

比如:auto f = []<typename T>( T t ) {};

这样 f 对象内部就有一个调用操作符模板。可以使用 f.template operator()<T>(t) 访问,当 T 能被推导时模板参数列表可以省略。与之前的 auto 类型参数相比,这项改动使得我们可以添加一些不能从函数参数中推导出来的模板参数(想一想 std::make_shared 函数模板也无法得知将要创建的对象类型,因而需要手动指定)。

有了这个功能,现在就可以用 decltype + lambda 去干以前类模板偏特化才能做到的匹配工作(本来用函数模板也可以,但是为了做一次匹配就要给函数模板取一个名字,比较麻烦):

#include <cstddef>
#include <utility>
#include <variant>

template <size_t N> struct A {};

template <size_t N>
using VariantOfEmptyClasses =
    decltype([]<size_t... Ns>(std::index_sequence<Ns...>) {
      return std::variant<A<Ns>...>{};
    }(std::make_index_sequence<N>()));

int main() {
  static_assert(sizeof(A<0>) == 1);
  static_assert(sizeof(VariantOfEmptyClasses<1>) == 2);
  static_assert(sizeof(VariantOfEmptyClasses<255>) == 2);
  static_assert(sizeof(VariantOfEmptyClasses<256>) == 4);
}

...args

还可以用 ...args= 初始化语法指定表达式,这样就可以自己选用移动或者转发,而以前版本捕获 args... 参数时只能通过复制(除非将他们包装在一个新的结构,比如 std::tuple 中)。

template <class F, class... Args> auto bind_all(F f, Args... args) {
  return [f = std::move(f), ... args = std::move(args)]() -> decltype(auto) {
    return f(args...);
  };
}

以前需要这样做:

template <class F, class... Args> auto bind_all(F f, Args... args) {
  return [f = std::move(f),
          t = std::tuple{std::move(args)...}]() -> decltype(auto) {
    std::apply(f, t);
  };
}

C++23

有了 explicit object parameter(毕竟 lambda 也是匿名对象)。

总体来看

现在 lambda 已经可以复杂成这样了(为了凑这个例子加了不少冗余约束):

#include <cstdio>
#include <type_traits>

template <typename T>
concept POD = std::is_standard_layout_v<T> && std::is_trivial_v<T>;

int main() {
    auto add_pod = []<POD T>
        requires POD<T>
                 [[nodiscard]] (T i, POD auto j) noexcept -> auto
                     requires POD<T>
    { return i + j; };

    printf("%f\n", add_pod(4.0, 2));
}

补充:实现递归

方案 1: std::function 类模板

lambda 出现之初就支持,因而以下代码仅需要 C++11 编译。在 lambda 表达式外创建一个函数对象,这样就能把自己捕获进来。

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

由于能够从 std::function 模板参数中得知 fibonacci 的返回类型,从而 lambda 函数体中所有表达式类型可知、函数返回值类型可知,trailing return type(也就是 ->)可以省略不写。

另外一方面,只有 fibonacci 的类型已经确定了,才能在 lambda 中调用它,所以即便是写上 lambda 的尾返回值,std::function 的模板参数也不能被推导出来,所以 std::function 的模板参数是不可以省略的。(std::function f = main; 就可以省略模板参数。)

缺点:函数的参数类型重复了两次,含有冗余信息。

方案 2:显式传入函数对象

由于使用了 generic lambda,以下代码需要支持 C++14 的编译器。

auto fibonacci = [&] (auto const &f, int i) -> int {
    return i < 2 ? i : f(f, i-1) + f(f, i-2);
};

调用的时候就要多传一个 fibonacci 函数对象本身作为参数了。为什么这里不像方案 2 一样需要提前知道函数的类型呢?因为 auto 推导(generic lambda)的代码生成过程是模板二阶段编译中的第二阶段。

这里的返回值类型也是必须要加的(不能是 -> auto),因为使用 f 时必须知道 f 的返回类型,不给全信息就无法确定函数中所有表达式的类型,从而无法实现模板函数的实例化。

缺点:写法不够直观,得到的函数对象在调用时需要额外将自己作为第一个参数传递。

方案 3:Deducing This

C++23 有了 explicit object parameter,不再需要捕获自己。现在可以这样实现:

// 在 MSVC 上编译成功(2023 年 5 月 24 日)
#include <iostream>
int main() {
    // 由于没有捕获任何变量,所以内部的 f 和外面的 f 重名也没有关系!
    auto f = [](this auto const &f, int a) -> int {
        return a > 0 ? a + f(a - 1) : 0;
    };
    std::cout << f(10) << '\n'; // prints 55
}