【问题标题】:Capturing by value in recursive lambda在递归 lambda 中按值捕获
【发布时间】:2017-09-30 02:49:46
【问题描述】:

我们可以定义像这样的递归 lambda 函数

std::function<void(int)> fun = [&fun](int a) {  if (a) fun(a - 1); };

然后我们可以调用它

fun(10);

但是,如果我将定义更改为

std::function<void(int)> fun = [fun](int a) {  if (a) fun(a - 1); };

然后尝试调用

fun(10);

出现分段错误。

有人可以解释为什么按引用捕获有效,而按值捕获会导致分段错误。

【问题讨论】:

    标签: c++ c++11 recursion lambda


    【解决方案1】:

    按值捕获是评估 lambda 表达式的一部分。那时,fun 仍未初始化,因为您仍在评估其初始化程序。只有在那之后才会初始化fun,但到那时副本已经发生了。

    最终结果是存储在 fun 中的 lambda 函数对象有一个名为 fun 的数据成员,它是未初始化的 std::function 的副本 — 未定义行为。

    【讨论】:

      【解决方案2】:

      std::function 用于递归 lambda 并不是一个好计划。在您的情况下,您会在其中包含 lambda 之前获得该函数的未初始化副本。

      这似乎很糟糕。运气好时未定义的行为会崩溃。

      递归 Lambdas

      假设我们希望将 Euclid 的 gcd() 写为 lambda。作为一个函数,它是:

      int gcd(int a, int b) {
          return b == 0 ? a : gcd(b, a%b);
      }
      

      但是 lambda 不能递归,它无法调用自身。 lambda 没有名称,在 lambda 的主体中使用 this 是指捕获的 this(假设 lambda 是在成员函数的主体中创建的,否则会出错)。那么我们该如何解决这个问题呢?

      使用std::function

      我们可以让 lambda 捕获对尚未构造的 std::function 的引用:

      std::function<int(int, int)> gcd = [&](int a, int b){
          return b == 0 ? a : gcd(b, a%b);
      };
      

      这可行,但应谨慎使用。它很慢(我们现在使用类型擦除而不是直接函数调用),它很脆弱(复制 gcd 或返回 gcd 会中断,因为 lambda 引用原始对象),并且它不适用于泛型拉姆达斯。

      使用两个智能指针:

      auto gcd_self = std::make_shared<std::unique_ptr< std::function<int(int, int)> >>();
      *gcd_self = std::make_unique<std::function<int(int, int)>>(
        [gcd_self](int a, int b){
          return b == 0 ? a : (**gcd_self)(b, a%b);
        };
      };
      

      这增加了很多间接性(这是开销),但它可以被复制/返回,并且所有副本共享状态。它确实允许您返回 lambda,并且比上述解决方案更脆弱。

      使用 Y 组合器

      借助一个简短的实用程序结构,我们可以解决所有这些问题:

      template <class F>
      struct y_combinator {
          F f; // the lambda will be stored here
      
          // a forwarding operator():
          template <class... Args>
          decltype(auto) operator()(Args&&... args) const {
              // we pass ourselves to f, then the arguments.
              // the lambda should take the first argument as `auto&& recurse` or similar.
              return f(*this, std::forward<Args>(args)...);
          }
      };
      // helper function that deduces the type of the lambda:
      template <class F>
      y_combinator<std::decay_t<F>> make_y_combinator(F&& f) {
          return {std::forward<F>(f)};
      }
      // (Be aware that in C++17 we can do better than a `make_` function)
      

      我们可以将gcd 实现为:

      auto gcd = make_y_combinator(
        [](auto&& gcd, int a, int b){
          return b == 0 ? a : gcd(b, a%b);
        }
      );
      

      y_combinator 是 lambda 演算中的一个概念,它允许您进行递归,而在定义之前无法命名自己。这正是 lambda 所面临的问题。

      您创建了一个将“recurse”作为其第一个参数的 lambda。当你想递归时,你将参数传递给递归。

      y_combinator 然后返回一个函数对象,该对象使用其参数调用该函数,但使用合适的“递归”对象(即y_combinator 本身)作为其第一个参数。它还将您调用 y_combinator 的其余参数转发给 lambda。

      简而言之:

      auto foo = make_y_combinator( [&](auto&& recurse, some arguments) {
        // write body that processes some arguments
        // when you want to recurse, call recurse(some other arguments)
      });
      

      并且您可以在没有严重限制或显着开销的 lambda 中进行递归。

      这个答案的一部分(递归 Lambda)最初是由 @Barry 在已失效的堆栈溢出文档中编写的。

      【讨论】:

      • 或者不要使用 lambda — 使用类或函数。
      • 对于 y-combinator,我必须指定 lambda 的返回值才能编译:[](auto&amp;&amp; gcd, int a, int b) -&gt; int
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-02-19
      • 1970-01-01
      相关资源
      最近更新 更多