Search code examples
c++lambdaclosureslanguage-lawyer

lambda capture, capturing an "undeclared variable"


I was surprised by a snippet in this C++ weekly.
I reproduced here the part that I don't understand:

int main() {
    auto accumulator = [sum = 0](int value) mutable {
        sum += value;
        return sum;
    };
    accumulator(1);          // expected return value: 1
    accumulator(2);          // expected return value: 1+2
    return accumulator(-1);  // expected return value: 1+2-1
}

The implicit expectation (in comment) is fulfilled but I don't recognize this capture syntax: sum does not exist in the lambda declaration scope, so it cannot capture that.
I know about this kind of syntax: [lhs = rhs](whatever){whatever_again}; that declares a lhs variable in the closure that is initialized by rhs value from the declaring scope, at the time of declaration. The fact that this code is working seems to mean that:

  • if the requested capture variable is not declared, then the capture is actually a definition;
  • the persistence of the value between calls, implies that sum is actually a member of the lambda closure;
  • sum is not const because the lambda is mutable.

Yet I cannot match these hypothesis with lambda cppreference.

I know about this kind of syntax: [lhs = rhs](whatever){whatever_again}; that declares a lhs variable in the closure that is initialized by rhs value from the declaring scope, at the time of declaration. The above behavior seems to indicate that rhs can be merely a value, and that the actual type of lhs is automatically deduced (I think that I get this point right from ClosureType::Captures, reproduced below). Is my understanding correct? And can someone point a reference or explain the cppreference wording?
I'm afraid that it rises a second question which is probably a duplicate (fill free to tell so or to ask me to split the post in 2): how is determined the type of the variables, inside the lambda body?

Notes

from ClosureType::Captures: type inside lambda body.

The type of each data member is the type of the corresponding captured entity, except if the entity has reference type (in that case, references to functions are captured as lvalue references to the referenced functions, and references to objects are captured as copies of the referenced objects).

also from ClosureType::Captures: closure non-static member variable:

the closure type includes unnamed non-static data members, declared in unspecified order, that hold copies of all entities that were so captured.

IMHO, if I extend the entity notion from lvalue to any kind of values, my hypothesis could be correct.


Solution

  • The implicit expectation (in comment) is fulfilled but I don't recognize this capture syntax: sum does not exist in the lambda declaration scope, so it cannot capture that.

    sum doesn't have to. sum is not the name of the entity we are capturing, it is just a name in the capture clause that is an alias for a data member of the lambda, and which we can use in the call operator. Only when sum has no initializer is sum also the name of the captured entity. This feature (capture clause with initializers) is called Generalized Captures.

    The above code snippet will make a lot more sense if we translate it into what happens "under the hood":

    int main() {
        // exposition-only, closure type is unnamed.
        struct __lambda {
            // exposition-only, data member is nameless.
            // lambda is mutable, so member is not const.
            int __sum;
            
            // return type will deduce to int.
            // mutable lambda means our call operator is not const-qualified.
            // call operator is automatically constexpr because it can be.
            constexpr auto operator()(int value) {
                __sum += value;
                return __sum;
            }
        };
    
        // We've used copy initialization in our capture, so copy initialization
        // must take place for the __sum member.
        // However, closure types are not aggregate types.
        auto accumulator = __lambda{.__sum = 0};
        accumulator(1);
        accumulator(2);
        return accumulator(-1);
    }
    

    You can see that [sum = 0] in our capture clause simply means that we have a member of type int in the closure type.

    The type of each data member is the type of the corresponding captured entity, except if the entity has reference type.

    - See cppreference

    In this case, the captured entity is the temporary object 0, which is of type int, so our data member is also of type int.


    See also: Your code on cppinsights.io