Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 446
Rez Avatar asked Aug 30 '19 09:08

Rez


People also ask

What is capture in lambda?

The capture clause is used to (indirectly) give a lambda access to variables available in the surrounding scope that it normally would not have access to. All we need to do is list the entities we want to access from within the lambda as part of the capture clause.

What is the correct statement about lambda expression?

What is the correct statement about lambda expression? Explanation: Return type in lambda expression can be ignored in some cases as the compiler will itself figure that out but not in all cases. Lambda expression is used to define small functions, not large functions.

What is the correct syntax for lambda expression in C++11?

Lambdas can both capture variables and accept input parameters. A parameter list (lambda declarator in the Standard syntax) is optional and in most aspects resembles the parameter list for a function. auto y = [] (int first, int second) { return first + second; };

Why do we need lambda expressions in C++?

One of the new features introduced in Modern C++ starting from C++11 is Lambda Expression. It is a convenient way to define an anonymous function object or functor. It is convenient because we can define it locally where we want to call it or pass it to a function as an argument.


3 Answers

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.

like image 107
463035818_is_not_a_number Avatar answered Oct 26 '22 19:10

463035818_is_not_a_number


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).

like image 28
n. 1.8e9-where's-my-share m. Avatar answered Oct 26 '22 21:10

n. 1.8e9-where's-my-share m.


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.

like image 2
Vlad from Moscow Avatar answered Oct 26 '22 20:10

Vlad from Moscow