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

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.

Answer

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.

Leave a Reply

Your email address will not be published. Required fields are marked *