递归的 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 引用原始对象而中断),并且它不适用于通用 lambdas。

使用两个智能指针:

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 所具有的问题。

你创建一个 lambda,将 recurse 作为其第一个参数。当你想要递归时,你传递参数 recurse。

然后 y_combinator 返回一个函数对象,该函数对象使用其参数调用该函数,但使用合适的 recurse 对象(即 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 递归,没有严重的限制或显着的开销。