c++lambdastaticlanguage-lawyermutable

Lambda static vs mutable variable


I have encountered a lambda with static variable in the following answer https://stackoverflow.com/a/79631994/2894535 :

EXPECT_CALL(my_mock, foo(_))
    .WillRepeatedly(InvokeWithoutArgs([](){
        static int counter = 0;
        constexpr static std::array<int, 2> values {13, 26};
        return values[counter++ % values.size()];
    }) );

My first intuition was to comment or edit the answer with a mutable counter instead:

[counter = 0]() mutable { ... }

My logic was that the static one could be unexpectedly shared between different users. For example, if the test was parametrized and the first instantiation called my_mock.foo() odd number of times, then the second one will start from 26 instead of 13.

On the second thought though, since each lambda creates an anonymous functor class, is that actually the case? I believe my confusion boils down to whether entering the same code twice will create a distinct functor with its separate statics, or reuse them.

This can be boiled down to the minimal example:

#include <cstdio>

auto static_lambda() {
    return []() { static int i = 0; return i++; }
}

auto mutable_lambda() {
    return [i = 0]() mutable { return i++; }
}

int main() {   
    auto s = static_lambda();
    std::printf("%d,", s());
    std::printf("%d,", s());

    auto s2 = static_lambda();
    std::printf("%d,", s2());
    std::printf("%d,", s2());

    auto m = mutable_lambda();
    std::printf("%d,", m());
    std::printf("%d,", m());

    auto m2 = mutable_lambda();
    std::printf("%d,", m2());
    std::printf("%d,", m2());
}

I have run this code and see it produces 0,1,2,3,0,1,0,1, meaning my initial intuition was correct, but am honestly still not sure why, for the language lawyer point of view.


Solution

  • A lambda's type is defined at the point where the lambda is created. So, there is only 1 lambda type defined by static_lambda(), and 1 lambda type defined by mutable_lambda(), although multiple instances of those types are created at runtime.

    Your code basically acts as-if you had written it something like this:

    #include <cstdio>
    
    auto static_lambda() {
        class _lambda_123 {
        public:
            int operator()() const {
                static int i = 0;
                return i++;
            }
        };
        return _lambda_123{};
    }
    
    auto mutable_lambda() {
        class _lambda_456 {
            int i = 0;
        public:
            int operator()() {
                return i++;
            }
        };
        return _lambda_456{};
    }
    
    int main() {   
        auto s = static_lambda();
        std::printf("%d,", s.operator()()); // acts on _lambda_123::operator()::i
        std::printf("%d,", s.operator()()); // acts on _lambda_123::operator()::i
    
        auto s2 = static_lambda();
        std::printf("%d,", s2.operator()()); // acts on _lambda_123::operator()::i
        std::printf("%d,", s2.operator()()); // acts on _lambda_123::operator()::i
    
        auto m = mutable_lambda();
        std::printf("%d,", m.operator()()); // acts on m.i
        std::printf("%d,", m.operator()()); // acts on m.i
    
        auto m2 = mutable_lambda();
        std::printf("%d,", m2.operator()()); // acts on m2.i
        std::printf("%d,", m2.operator()()); // acts on m2.i
    }
    

    Like any other static variable declared in any function, all invocations of the static_lambda's function-call operator will act on a single static i variable in memory, whereas all instances of the mutable_lambda will capture a local copy of i for its function-call operator to act on.