在递归lambda中捕获值

时间:2017-03-15 15:17:03

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

我们可以定义像

这样的递归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);

发生分段错误。

有人可以解释为什么通过引用捕获工作而按值捕获会产生分段错误。

2 个答案:

答案 0 :(得分:8)

按值计算,作为评估lambda表达式的一部分进行评估。那时,fun仍未被初始化,因为您仍在评估其初始化。仅在此之后fun被初始化,但到那时复制已经发生。

净效应是存储在fun内的lambda函数对象有一个名为fun的数据成员,它是未经初始化的std::function - 未定义行为的副本。

答案 1 :(得分:1)

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引用原始对象而中断),它赢了使用通用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-combinator

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

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

这个答案的一部分(The Recursive Lambda)最初由@Barry写在已解散的Stack Overflow Documentation上。