Marco Luzzara
Marco Luzzara

Reputation: 6026

lambda's captured variable is reset

I'm trying to use lambdas inside a project but I think I'm missing something about the closure's scope. I tested this piece of code which in some way is a simplification of my problem.

#include <iostream>
#include <functional>

using namespace std;

void tester_wrapper(std::function<int(void)> cb, int counter){
    if (counter == 10)
        return;
    else{
        cout << cb() << endl;
        tester_wrapper(cb, counter + 1);
    }
}

void tester(std::function<int(void)> cb){
    tester_wrapper(cb, 0);
}

int main()
{
    auto getNum = ([](int starter) {
        return [starter]() mutable {
            return ++starter;
        };
    })(1);

    tester(getNum);
    tester(getNum);
}

After the first call to tester the captured variable starter is reset so that the same output is printed twice.

What should I do in order to avoid this behaviour of the inner counter(starter) of the lambda? Essentially the second call to tester has to print 10 numbers starting from 12 instead of 2.


EDIT

Thank you for the answers. I have not considered that I was passing a copy to tester_wrapper, so I've found this solution:

#include <iostream>
#include <functional>

using namespace std;

std::function<int(void)> mylambda(int starter){
    return [starter]() mutable {
        return ++starter;
    };
}

void tester_wrapper(const std::function<int(void)>& cb, int counter){
    if (counter == 10)
        return;
    else{
        cout << cb() << endl;
        tester_wrapper(cb, counter + 1);
    }
}

void tester(const std::function<int(void)>& cb){
    tester_wrapper(cb, 0);
}

int main()
{
    /*auto getNum = ([](int starter) {
        return [starter]() mutable {
            return ++starter;
        };
    })(1);*/

    auto getNum = mylambda(1);

    tester(getNum);
    tester(getNum);
}

However, now I can't understand why the old getNum print the same output while it's different using an "external" function, which is mylambda.

(Am I supposed to post a new question for this?)

Upvotes: 2

Views: 500

Answers (3)

The variable isn't reset, it's a different copy of the variable. In fact, there are a bunch copies. The first is in the state of the lambda you create. The second is created when the first std::function is constructed. You must remember that it copies the callable it receives into itself. So each invocation of tester starts a chain of copies. One way to get around it, is to pass the lambda inside a std::reference_wrapper.

tester(std::ref(getNum));
tester(std::ref(getNum));

The reference wrapper will be copied, but all copies will refer to the same lambda object, getNum.

Now, assuming you intend to create many different objects like getNum, a std::function and the type erasure it provides are a reasonable course of action to avoid possible code bloat. The thing to remember is to not create superfluous copies. So tester accepting by value is legitimate, but tester_wrapper should accept by reference instead. That way, you'll only pay for the type erasure in the one place you need it, at the API boundary.

Upvotes: 3

Edgar Rokjān
Edgar Rokjān

Reputation: 17483

One possible solution is to create a separate variable starter and then capture it by reference so the lambda changes the actual variable:

auto starter = 0;

auto getNum = [&starter]() {
    return ++starter;
};

Then you just call:

tester(getNum);
tester(getNum);

The output will be numbers from 1 to 20.

Upvotes: 3

Sarfaraz Nawaz
Sarfaraz Nawaz

Reputation: 361342

The argument getNum you're passing to tester is being copied by std::function<int(void)>. That is, std::function<int(void)> does not store the original getNum, instead it stores a copy.

@StoryTeller and @Edgar already suggested two solutions. Here is third one:

template<typename Callback>
void tester_wrapper(Callback && cb, int counter){
    if (counter == 10)
        return;
    else{
        cout << cb() << endl;
        tester_wrapper(cb, counter + 1);
    }
}

template<typename Callback>
void tester(Callback && cb){
    tester_wrapper(std::forward<Callback>(cb), 0);
}

Now there is no copy, as both functions accept the argument by reference.

By the way, this code is likely to be faster than the other two, as other two continue to use std:function which has one virtual call or equivalent to invoke the stored callable.

Hope that helps.

Upvotes: 2

Related Questions