C++

Lambda Expressions in C++

Why Lambda Expression?

Consider the following statement:

    int myInt = 52;

Here, myInt is an identifier, an lvalue. 52 is a literal, a prvalue. Today, it is possible to code a function specially and put it in the position of 52. Such a function is called a lambda expression. Consider also the following short program:

#include <iostream>

using namespace std;

int fn(int par)

    {

        int answer = par + 3;

        return answer;

    }


int main()

{

    fn(5);

   

    return 0;

}

Today, it is possible to code a function specially and put it in the position of the argument of 5, of the function call, fn(5). Such a function is called a lambda expression. The lambda expression (function) in that position is a prvalue.

Any literal except the string literal is a prvalue. The lambda expression is a special function design that would fit as a literal in code. It is an anonymous (unnamed) function. This article explains the new C++ primary expression, called the lambda expression. Basic knowledge in C++ is a requirement to understand this article.

Article Content

Illustration of Lambda Expression

In the following program, a function, which is a lambda expression, is assigned to a variable:

#include <iostream>

using namespace std;

auto fn = [](int param)

            {

                int answer = param + 3;

                return answer;

            };


int main()

{

    auto variab = fn(2);

    cout << variab << '\n';


    return 0;

}

The output is:

    5

Outside the main() function, there is the variable, fn. Its type is auto. Auto in this situation means that the actual type, such as int or float, is determined by the right operand of the assignment operator (=). On the right of the assignment operator is a lambda expression. A lambda expression is a function without the preceding return type. Note the use and position of the square brackets, []. The function returns 5, an int, which will determine the type for fn.

In the main() function, there is the statement:

    auto variab = fn(2);

This means, fn outside main(), ends up as the identifier for a function. Its implicit parameters are those of the lambda expression. The type for variab is auto.

Note that the lambda expression ends with a semicolon, just like the class or struct definition, ends with a semicolon.

In the following program, a function, which is a lambda expression returning the value of 5, is an argument to another function:

#include <iostream>

using namespace std;

void otherfn (int no1, int (*ptr)(int))

{

        int no2 = (*ptr)(2);

        cout << no1 << ' ' << no2 << '\n';

    }


int main()

{

    otherfn(4, [](int param)

            {

                int answer = param + 3;

                return answer;

            });


    return 0;
}

The output is :

    4  5

There are two functions here, the lambda expression and the otherfn() function. The lambda expression is the second argument of the otherfn(), called in main(). Note that the lambda function (expression) does not end with a semicolon in this call because, here, it is an argument (not a stand-alone function).

The lambda function parameter in the definition of the otherfn() function is a pointer to a function. The pointer has the name, ptr. The name, ptr, is used in the otherfn() definition to call the lambda function.

The statement,

    int no2 = (*ptr)(2);

In the otherfn() definition, it calls the lambda function with an argument of 2. The return value of the call, "(*ptr)(2)" from the lambda function, is assigned to no2.

The above program also shows how the lambda function can be used in the C++ callback function scheme.

Parts of Lambda Expression

The parts of a typical lambda function is as follows:

    [] () {}
  • [] is the capture clause. It can have items.
  • () is for the parameter list.
  • {} is for the function body. If the function is standing alone, then it should end with a semicolon.

Captures

The lambda function definition can be assigned to a variable or used as the argument to a different function call. The definition for such a function call should have as a parameter, a pointer to a function, corresponding to the lambda function definition.

The lambda function definition is different from the normal function definition. It can be assigned to a variable in the global scope; this function-assigned-to-variable can also be coded inside another function. When assigned to a global scope variable, its body can see other variables in the global scope. When assigned to a variable inside a normal function definition, its body can see other variables in the function scope only with the capture clause’s help, [].

The capture clause [], also known as the lambda-introducer, allows variables to be sent from the surrounding (function) scope into the lambda expression’s function body. The lambda expression’s function body is said to capture the variable when it receives the object. Without the capture clause [], a variable cannot be sent from the surrounding scope into the lambda expression’s function body. The following program illustrates this, with the main() function scope, as the surrounding scope:

#include <iostream>

using namespace std;

int main()

{

    int id = 5;


    auto fn = [id]()

            {

                cout << id << '\n';

            };

    fn();


    return 0;

}

The output is 5. Without the name, id, inside [], the lambda expression would not have seen the variable id of the main() function scope.

Capturing by Reference

The above example use of the capture clause is capturing by value (see details below). In capturing by reference, the location (storage) of the variable, e.g., id above, of the surrounding scope, is made available inside the lambda function body. So, changing the value of the variable inside the lambda function body will change the value of that same variable in the surrounding scope. Each variable repeated in the capture clause is preceded by the ampersand (&) to achieve this. The following program illustrates this:

#include <iostream>

using namespace std;

int main()

{

    int id = 5; float ft = 2.3; char ch = 'A';

    auto fn = [&id, &ft, &ch]()

            {

                id = 6; ft = 3.4; ch = 'B';

            };

    fn();

    cout << id << ", " <<  ft << ", " <<  ch << '\n';

    return 0;

}

The output is:

    6, 3.4, B

Confirming that the variable names inside the lambda expression’s function body are for the same variables outside the lambda expression.

Capturing by Value

In capturing by value, a copy of the variable’s location, of the surrounding scope, is made available inside the lambda function body. Though the variable inside the lambda function body is a copy, its value cannot be changed inside the body as of now. To achieve capturing by value, each variable repeated in the capture clause is not preceded by anything. The following program illustrates this:

#include <iostream>

using namespace std;

int main()

{

    int id = 5; float ft = 2.3; char ch = 'A';

    auto fn = [id, ft, ch]()

            {

               //id = 6; ft = 3.4; ch = 'B';

               cout << id << ", " <<  ft << ", " <<  ch << '\n';

            };

    fn();

    id = 6; ft = 3.4; ch = 'B';

    cout << id << ", " <<  ft << ", " <<  ch << '\n';

    return 0;

}

The output is:

    5, 2.3, A

    6, 3.4, B

If the comment indicator is removed, the program will not compile. The compiler will issue an error message that the variables inside the function body’s definition of the lambda expression cannot be changed. Though the variables cannot be changed inside the lambda function, they can be changed outside the lambda function, as the above program’s output shows.

Mixing Captures

Capturing by reference and capturing by value can be mixed, as the following program shows:

#include <iostream>

using namespace std;

int main()

{

    int id = 5; float ft = 2.3; char ch = 'A'; bool bl = true;


    auto fn = [id, ft, &ch, &bl]()

            {

                ch = 'B'; bl = false;

                cout << id << ", " <<  ft << ", " <<  ch << ", " <<  bl << '\n';

            };

    fn();


    return 0;

}

The output is:

    5, 2.3, B, 0

When all captured, are by reference:

If all variables to be captured are captured by reference, then just one & will suffice in the capture clause. The following program illustrates this:

#include <iostream>

using namespace std;

int main()

{

    int id = 5; float ft = 2.3; char ch = 'A'; bool bl = true;


    auto fn = [&]()

            {

                id = 6; ft = 3.4; ch = 'B'; bl = false;

            };

    fn();

    cout << id << ", " <<  ft << ", " <<  ch << ", " <<  bl << '\n';


    return 0;

}

The output is:

    6, 3.4, B, 0

If some variables are to be captured by reference and others by value, then one & will represent all the references, and the rest will each not be preceded by anything, as the following program shows:

using namespace std;

int main()

{

    int id = 5; float ft = 2.3; char ch = 'A'; bool bl = true;


    auto fn = [&, id, ft]()

            {

                ch = 'B'; bl = false;

                cout << id << ", " <<  ft << ", " <<  ch << ", " <<  bl << '\n';

            };

    fn();


    return 0;

}

The output is:

    5, 2.3, B, 0

Note that & alone (i.e., & not followed by an identifier) has to be the first character in the capture clause.

When all captured, are by value:

If all variables to be captured are to be captured by value, then just one = will suffice in the capture clause. The following program illustrates this:

#include <iostream>

using namespace std;

int main()
{

    int id = 5; float ft = 2.3; char ch = 'A'; bool bl = true;


    auto fn = [=]()

            {

                cout << id << ", " <<  ft << ", " <<  ch << ", " <<  bl << '\n';

            };

    fn();


    return 0;


}

The output is:

    5, 2.3, A, 1

Note: = is read-only, as of now.

If some variables are to be captured by value and others by reference, then one = will represent all the read-only copied variables, and the rest will each have &, as the following program shows:

#include <iostream>

using namespace std;

int main()

{

    int id = 5; float ft = 2.3; char ch = 'A'; bool bl = true;


    auto fn = [=, &ch, &bl]()

            {

                ch = 'B'; bl = false;

                cout << id << ", " <<  ft << ", " <<  ch << ", " <<  bl << '\n';

            };

    fn();


    return 0;

}

The output is:

    5, 2.3, B, 0

Note that = alone has to be the first character in the capture clause.

Classical Callback Function Scheme with Lambda Expression

The following program shows how a classical callback function scheme can be done with the lambda expression:

#include <iostream>

using namespace std;

char *output;


auto cba = [](char out[])

    {

        output = out;

    };

 

void principalFunc(char input[], void (*pt)(char[]))

    {

        (*pt)(input);

        cout<<"for principal function"<<'\n';

    }


void fn()

    {

        cout<<"Now"<<'\n';

    }


int main()

{

    char input[] = "for callback function";

    principalFunc(input, cba);

    fn();

    cout<<output<<'\n';

 

   return 0;

}

The output is:

    for principal function

    Now

    for callback function

Recall that when a lambda expression definition is assigned to a variable in the global scope, its function body can see global variables without employing the capture clause.

The trailing-return-type

The return type of a lambda expression is auto, meaning the compiler determines the return type from the return expression (if present). If the programmer really wants to indicate the return type, then he will do it as in the following program:

#include <iostream>

using namespace std;

auto fn = [](int param) -> int

            {

                int answer = param + 3;

                return answer;

            };


int main()

{

    auto variab = fn(2);

    cout << variab << '\n';


    return 0;

}

The output is 5. After the parameter list, the arrow operator is typed. This is followed by the return type (int in this case).

Closure

Consider the following code segment:

struct Cla

    {

        int id = 5;

        char ch = 'a';

    } obj1, obj2;

Here, Cla is the name of the struct class.  Obj1 and obj2 are two objects that will be instantiated from the struct class. Lambda expression is similar in implementation. The lambda function definition is a kind of class. When the lambda function is called (invoked), an object is instantiated from its definition. This object is called a closure. It is the closure that does the work the lambda is expected to do.

However, coding the lambda expression like the struct above will have obj1 and obj2 replaced by the corresponding parameters’ arguments. The following program illustrates this:

#include <iostream>

using namespace std;

auto fn = [](int param1, int param2)

            {

                int answer = param1 + param2;

                return answer;

            } (2, 3);


int main()

{

    auto var = fn;

    cout << var << '\n';


    return 0;

}

The output is 5. The arguments are 2 and 3 in parentheses. Note that the lambda expression function call, fn, does not take any argument since the arguments have already been coded at the end of the lambda function definition.

Conclusion

The lambda expression is an anonymous function. It is in two parts: class and object. Its definition is a kind of class. When the expression is called, an object is formed from the definition. This object is called a closure. It is the closure that does the work the lambda is expected to do.

For the lambda expression to receive a variable from an outer function scope, it needs a non-empty capture clause into its function body.

About the author

Chrysanthus Forcha

Discoverer of mathematics Integration from First Principles and related series. Master’s Degree in Technical Education, specializing in Electronics and Computer Software. BSc Electronics. I also have knowledge and experience at the Master’s level in Computing and Telecommunications. Out of 20,000 writers, I was the 37th best writer at devarticles.com. I have been working in these fields for more than 10 years.