In lambda functions syntax, what purpose does a 'capture list' serve?

前端 未结 5 1547
一个人的身影
一个人的身影 2020-12-24 04:00

Taken from an answer to this question, as an example, this is a code that calculates the sum of elements in a std::vector:

std::for_each(
    ve         


        
相关标签:
5条回答
  • 2020-12-24 04:09

    Consider this:

    std::function<int()>
    make_generator(int const& i)
    {
        return [i] { return i; };
    }
    
    // could be used like so:
    int i = 42;
    auto g0 = make_generator(i);
    i = 121;
    auto g1 = make_generator(i);
    i = 0;
    
    assert( g0() == 42 );
    assert( g1() == 121 );
    

    Notice that in this situation the two generators that have been created each have their own i. This is not something you can recreate with ordinary functions, and thus why those don't use capture lists. Capture lists solve one version of the funarg problem.

    Are lambda functions more than just nameless functions?

    That is a very smart question. What lambda expressions create are in fact more powerful than regular functions: they're closures (and the Standard indeed refer to the objects that lambda expressions create as 'closure objects'). To put it briefly, a closure is a function combined with a bound scope. C++ syntax has chosen to represent the function bit in a familiar form (argument list with delayed return type with function body, some parts optional) while the capture list is the syntax that specifies which local variable will participate in the bound scope (non-local variables are automatically introduced).

    Note that other languages with closures usually do not have a construct similar to C++ capture lists. C++ has made the design choice of capture-lists due to its memory model (local variable only live as long as the local scope) and its philosophy of not paying for what you don't use (making local variables now automagically live longer if they're captured might not be desirable in each and every case).

    0 讨论(0)
  • 2020-12-24 04:10

    It's perhaps better to think about a lambda expression as an object that happens to have () operator, rather than just a function. The lambda "object" can have fields that remember (or "capture") the out-of-lambda variables at the time of lambda construction, to be used later at the time of lambda execution.

    The capture list is simply a declaration of such fields.

    (You don't even need to specify the capture list yourself - The [&] or [=] syntax instructs the compiler to determine the capture list automatically, based on which variables from the outer scope are used in the lambda body.)

    A normal function cannot contain state - it cannot "remember" arguments at one time to be used at another. A lambda can. A manually crafted class with user-implemented () operator (aka. "functor") also can, but is far less convenient syntactically.

    0 讨论(0)
  • 2020-12-24 04:12

    From the syntax link you gave, the capture list "defines what from the outside of the lambda should be available inside the function body and how"

    Ordinary functions can use external data in a few ways:

    1. Static fields
    2. Instance fields
    3. Parameters (including reference parameters)
    4. Globals

    Lambda add the ability to have one unnamed function within another. The lambda can then use the values you specify. Unlike ordinary functions, this can include local variables from an outer function.

    As that answer says, you can also specify how you want to capture. awoodland gives a few exampls in another answer. For instance, you can capture one outer variable by reference (like a reference parameter), and all others by value:

    [=, &epsilon]
    

    EDIT:

    It's important to distinguish between the signature and what the lambda uses internally. The signature of a lambda is the ordered list of parameter types, plus the type of the returned value.

    For instance, a unary function takes a single value of a particular type, and returns a value of another type.

    However, internally it can use other values. As a trivial example:

    [x, y](int z) -> int 
    {
       return x + y - z;
    }
    

    The caller of the lambda only knows that it takes an int and returns an int. However, internally it happens to use two other variables by value.

    0 讨论(0)
  • 2020-12-24 04:18

    Are lambda functions more than just nameless functions?

    YES! Apart from being nameless, they can refer to the variables in the lexically enclosed scope. In your case, an example would be the sum_of_elems variable (its neither a parameter nor a global variable I suppose). Normal functions in C++ can't do that.

    What extra information does a capture list provide?

    The capture list provides the

    1. list of variables to be captured with
    2. information how should they be captured (by reference/by value)

    In other (eg. functional) languages, this isn't necessary, because they always refer to values in one fashion (eg. if values are immutable, the capture would be by value; other possibility is that everything is a variable on the heap so everything is captured by refrence and you needn't worry about its lifetime etc.). In C++, you have to specify it to choose between reference (can change the variable outside, but will blow up when the lambda outlives the variable) and value (all changes isolated inside the lambda, but will live as long as the lambda - basically, it will be a field in a structure representing the lambda).

    You can make the compiler to capture all the needed variables by using the capture-default symbol, which just specifies the default capture mode (in your case: & => reference; = would be value). In that case, basically all variables referenced in the lambda from the outer scope are captured.

    0 讨论(0)
  • 2020-12-24 04:20

    The basic problem that we're trying to solve is that some algorithm expects a function that only takes a particular set of arguments (one int in your example). However, we want the function to be able to manipulate or inspect some other object, maybe like so:

    void what_we_want(int n, std::set<int> const & conditions, int & total)
    {
        if (conditions.find(n) != conditions.end()) { total += n; }
    }
    

    However, all we are allowed to give our algorithm is a function like void f(int). So where do we put the other data?

    You could either keep the other data in a global variable, or you could follow the traditional C++ approach and write a functor:

    struct what_we_must_write
    {
        what_we_must_write(std::set<int> const & s, int & t)
        : conditions(s), total(t)
        {   }
    
        void operator()(int n)
        {
            if (conditions.find(n) != conditions.end()) { total += n; }
        }
    private:
       std::set<int> const & conditions;
       int & total;
    };
    

    Now we can call the algorithm with a suitably initialized functor:

    std::set<int> conditions;
    int total;
    
    for_each(v.begin(), v.end(), what_we_must_write(conditions, total));
    

    Finally, a closure object (which is described by a lambda expression) is just that: A short-hand way of writing a functor. The equivalent of the above functor is the lambda

    auto what_we_get = [&conditions, &total](int n) -> void {
        if (condiditons.find(n) != conditions.end()) { total += n; } };
    

    The short-hand capture lists [=] and [&] just capture "everything" (respectively by value or by reference), which means that the compiler figures out the concrete capture list for you (it doesn't actually put everything into the closure object, but only the things you need).

    So, in nutshell: A closure object without capture is like a free function, and a closure with capture is like a functor object with suitably defined and initialized private member objects.

    0 讨论(0)
提交回复
热议问题