如何管理自引用跨范围 lambda?

How does one manage a self-referring cross-scope lambda?

假设我有一个我想在计时器结束时调用的功能。我已将那部分功能放入 lambda 函数中。此外,在那个函数中,我可能希望设置另一个计时器以在以后的另一个场合调用同一个 lambda。

void doSetupThingsInSomeDecoupledCodeOrWhatever() {
    std::function<void(float)> semiRecursiveFunc;
    semiRecursiveFunc = [&semiRecursiveFunc](float deltaT){
        if (whatever()) {
            // Do something...
        }
        else {
            // Do something else, then:
            float durationMS = getRNGSystem().getUniformFloat(1000.0f, 5000.0f)
            // Gives the timer a duration to wait, and a function to run at the end of it.
            getTimerSystem().setNewTimer(durationMS, semiRecursiveFunc);
        }
    };

    float durationMS = getRNGSystem().getUniformFloat(1000.0f, 5000.0f)
    // Gives the timer a duration to wait, and a function to run at the end of it.
    getTimerSystem().setNewTimer(durationMS, fooLambda);
}

现在,显然这行不通,因为 semiRecursiveFunc 绑定到 doSetupThingsInSomeDecoupledCodeOrWhatever 的范围,当计时器系统尝试 运行 时,该功能将不再存在,一切都会分解成壮观的火球。

管理此问题的最佳方法是什么?我不能将 semiRecursiveFunc 存储在指针中,因为据我所知,不能像那样声明 lambda。这种持久性 lambda 用例是否有一些通用工具?什么是最不丑陋的方法,周围的基础设施最少?是否有最佳实践可供遵循,我错过了一些相关工具?任何建议或建议将不胜感激。

您要查找的是a y-combinator, sometimes called a fixed-point combinator

无论哪种方式,而不是完全使用 std::function(这会增加不必要的开销),您可以像这样编写回调:

auto semiRecursiveCallback = combinator([](auto self, float deltaT){
    if (whatever()) {
        // Do something...
    }
    else {
        // Do something else, then:
        float durationMS = getRNGSystem().getUniformFloat(1000.0f, 5000.0f)
        // Gives the timer a duration to wait, and a function to run at the end of it.
        // NB: we pass 'self' as the argument
        getTimerSystem().setNewTimer(durationMS, self);
    }
});

其中 combinator 是我的链接答案的 y_combinator 实现或来自优秀 Boost.HOF 库的 boost::hof::fix

组合子确保对象本身可以访问它自己,所以你可以做递归的事情。在上面的代码中,你实际上得到了你自己的副本,但这很好:值语义很酷。

这是一个微型 Y 组合器:

template<class R>
auto Y = [] (auto f) {
  auto action = [=] (auto action) {
    return [=] (auto&&... args)->R {
      return f( action(action),decltype(args)(args)... );
    };
  };
  return action(action);
};

只需这样做:

auto semiRecursiveFunc = Y<void>([](auto&& semiRecursiveFunc, float deltaT){
    if (whatever()) {
        // Do something...
    }
    else {
        // Do something else, then:
        float durationMS = getRNGSystem().getUniformFloat(1000.0f, 5000.0f)
        // Gives the timer a duration to wait, and a function to run at the end of it.
        getTimerSystem().setNewTimer(durationMS, semiRecursiveFunc);
    }
);

而且有效。

Y<R> 将一个可调用对象作为第一个参数传递给递归对象。递归时,只需传递其余参数即可。

您可以编写更高级的 Y 组合器。这个复制 lambdas 状态 a lot 并且不挑剔地移动它,以保持其实现简单。它还要求您提供其 return 类型(由于 C++ 类型推导规则,这很难避免)。

这是一种Objective-C引用计数风格的方法。优点是您可以使用与您想要的原始函数相同的 lambda 签名(没有额外的参数)。缺点是它看起来丑陋且冗长,而且你必须始终通过 shared_ptr; 使用 lambda;不能单独拿出来传。

void doSetupThingsInSomeDecoupledCodeOrWhatever() {
    std::shared_ptr<std::weak_ptr<std::function<void(float)>>> weakFuncHolder =
            std::make_shared<std::weak_ptr<std::function<void(float)>>>();
    std::shared_ptr<std::function<void(float)>> semiRecursiveFunc =
            std::make_shared<std::function<void(float)>>([=](float deltaT) {
        std::shared_ptr<std::function<void(float)>> strongFunc(*weakFuncHolder);
        if (whatever()) {
            // Do something...
        }
        else {
            // Do something else, then:
            float durationMS = getRNGSystem().getUniformFloat(1000.0f, 5000.0f);
            // Gives the timer a duration to wait, and a function to run at the end of it.
            getTimerSystem().setNewTimer(durationMS,
                    [=](float deltaT){ (*strongFunc)(deltaT); });
        }
    });
    *weakFuncHolder = semiRecursiveFunc;

    float durationMS = getRNGSystem().getUniformFloat(1000.0f, 5000.0f);
    // Gives the timer a duration to wait, and a function to run at the end of it.
    getTimerSystem().setNewTimer(durationMS,
            [=](float deltaT){ (*semiRecursiveFunc)(deltaT); });
}