1

Assume that I have a functionality which I want to call whenever a timer finishes. I have put that piece of functionality in a lambda function. Furthermore, in that function, I may wish to set another timer to call that same lambda on another, later occasion.

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);
}

Now, clearly this won't work, because semiRecursiveFunc is tied to the scope of doSetupThingsInSomeDecoupledCodeOrWhatever, and when the timer system tries to run it the function will no longer exist and everything will disintegrate into a spectacular ball of flame.

What's the best way to manage this? I can't store semiRecursiveFunc in a pointer because one can't declare lambdas like that, as far as I can tell. Is there some common tool for this sort of persistent-lambda use-case? What's the least ugly approach, with minimum surrounding infrastructure? Is there a best-practice to follow, some relevant tool I've missed? Any suggestions or recommendations would be much appreciated.

Stefan Bauer
  • 393
  • 1
  • 9

3 Answers3

1

What you're looking for is a y-combinator, sometimes called a fixed-point combinator.

Either way, instead of using std::function at all (which adds needless overhead), you would write your callback like this:

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);
    }
});

Where combinator is either the y_combinator implementation of my linked answer or boost::hof::fix from the excellent Boost.HOF library.

The combinator ensures that the object itself has access to itself, so you can do recursive things. In the above code, you're actually getting passed a copy of yourself, but that's fine: value semantics are cool like that.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • Wouldn't it be easier to just not make it be a lambda, just make it a class external to the function? Also, are you sure you want to take `self` by value? – Nicol Bolas Feb 02 '21 at 17:29
  • @NicolBolas It depends (that question applies to all lambdas, no?) and it depends (that question to applies to all parameters, no? As presented, the lambda doesn't capture anything, so there's no reason not to take it by value) – Barry Feb 02 '21 at 17:41
1

Here is a tiny Y-combinator:

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);
};

Just do this:

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);
    }
);

and it works.

Y<R> takes a callable that is passed what to recurse on as its first argument. When you recurse, just pass the rest of the arguments.

You can write a fancier Y combinator. This one copies the lambdas state a lot and isn't picky about moving it, to keep its implementation simple. It also requires you provide its return type (that is harder to avoid, due to C++ type deduction rules).

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
0

Here's a way that is in the style of Objective-C reference counting. The advantage is that you can use a lambda signature that is the same as the original function you want (no extra arguments). The disadvantages are that it looks ugly and verbose, and you have to always use the lambda through a shared_ptr; you can't take it out and pass it separately.

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); });
}
newacct
  • 119,665
  • 29
  • 163
  • 224