C++ Lambda Expressions: Capture Clause vs Argument List; what is the crucial difference?
Asked Answered
F

3

14

I am learning about Lambda Expressions in C++ although I am not a newcomer to C/C++. I am having difficulty seeing the relative merits of using the Capture-Clause vs old fashioned parameter passing in the Argument-List to draw variables into the Lambda body for manipulation. I am familiar with their syntactical differences and what is and is not allowed in each, but just don't see how one is more effective than the other?

If you have insider knowledge, or a better picture of what's going on with Lambdas please let me know.

Many Thanks, Reza.

Firearm answered 30/8, 2019 at 9:56 Comment(2)
One of the main motivations for Lambda's is the the predicate parameter that many of the std methods use (eg find_if etc). The lambda condenses the syntax to allow the predicate and lambda to be in the same expression. Previously this would require a class (template) decaration (at global scope), initialisation of the class object and final passing the object into the predicate.Marry
The difference is a matter of time. The capture arguments are determined when the lambda is created, the function arguments when it is called.Altair
H
5

Consider that lambdas are basically just syntactic sugar for functors. For example

int x = 1;
auto f = [x](int y){ return x+y; };

is more or less equivalent to

struct add_x {
    int x;
    add_x(int x) : x(x) {}
    int operator()(int y) const { return x+y; }
}

int x = 1;
add_x f{x};

And the difference becomes apparent when you pass the lambda around, e.g.

template <typename F> 
void foo(F f) {
    for (int i=0;i<10;++i) std::cout << f(i) << '\n';
}

Functions like that are one of the main motiviations to use lambdas and it is the function that (in this case only implicitly) specifies the expected signature. You can call foo as

foo(f);

But if your functor / lambda would take also x as parameter then you would not be able to pass it to foo.

TL;DR: Variables that are captured consitute the state of the lambda, while parameters are just like ordinary function parameters.

Hetrick answered 30/8, 2019 at 10:27 Comment(0)
O
2

The difference is that the same capture can be used with different arguments.

Consider the following simple example

#include <iostream>
#include <iterator>
#include <algorithm>

int main() 
{
    int a[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    const int N = 10;

    for ( const auto &item : a ) std::cout << item << ' ';
    std::cout << '\n';

    std::transform( std::begin( a ), std::end( a ), std::begin( a ),
                    [=]( const auto &item ) { return N * item; } );

    for ( const auto &item : a ) std::cout << item << ' ';
    std::cout << '\n';

    return 0;
}

The program output is

0 1 2 3 4 5 6 7 8 9 
0 10 20 30 40 50 60 70 80 90 

the arguments for the lambda are supplied by the algorithm std::transform. The algorithm is unable to pass to the lambda the multiplier N. So you need to capture it and the multiplier will be used with any argument passed to the lambda.

Ochlocracy answered 30/8, 2019 at 9:57 Comment(0)
E
2

A lambda expression creates a function-like object with some optional additional state. The call signature is determined by the lambda parameters, and the additional state is determined by the capture clause.

Now the signature you need to create is not always your choice. If you are passing your lambda to a standard or third-party API, then the API requires your lambda to have a certain signature. If tgere is any data you want to pass in addition to the imposed signature, you need to capture it.

Consider a well known example from the C library: the qsort function.

void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*));

The comparator function accepts pointers to the two objects being compared and that's it. There is no way to pass an additional flag that would control how exactly the comparison is done. As an example, consider sorting a list of words in some natural language according to the collation rules of that language, determined at runtime. How do you tell your comparator which language to use? The only option with this API is to set the language in a static variable (yikes).

Because of this well known shortcoming, people are defining various non-standard substitute APIs. For example

void qsort_r(void *base, size_t nmemb, size_t size,
       int (*compar)(const void *, const void *, void *),
       void *arg);

I hope you recognise what's going on. You pass an additional argument (the language identifier or whatever) as arg, then the sort function forwards it as a sealed package to your comparator. It then casts the argument to its original type and uses it

Enter C++. In std::sort, the comparator is a function like object that carries its own state. So this trick is unnecessary. You define something like

struct LanguageSensitiveComparator
{
    LanguageSensitiveComparator(LangauageID lang) : lang(lang) {}
    LangauageID lang;
    bool operator()(const string& a, const string& b) const { .... }  // etc
};

sort(dict.begin(), dict.end(), LanguageSensitiveComparator(lang));

C++11 takes its a step further. Now you can define the function object on the spot, using a lambda.

sort (begin(dict), end(dict),
          [=lang](const string& a, const string& b) { ..  });

Back to your question. Could you pass lang as an argument instead of capturing it? Sure, but you would need to define your own sort that knows about an additional LabguageID parameter (that's what qsort_r basically does, except it's not type safe).

Edenedens answered 30/8, 2019 at 12:1 Comment(1)
Thank you ! This is the only BS free answer out there, right on point!Gegenschein

© 2022 - 2024 — McMap. All rights reserved.