Lambda Expressions in C++ 11 – Part 1 (Definition and Syntax)

  The C++ 11 standard introduced several useful features, including Automatic Type Detection, nullptr, lambda expressions etc. Here, we shall discuss the meaning, syntax, usage and significance of lambda expressions in C++.

What is a Lambda Expression?

A lambda expression is, in simple terms, an anonymous function, i.e. a function that has no name. It has a body, it accepts arguments and even returns a value. It can access the variables that are accessible to the enclosing scope. Its main feature is that it can be used to define a function whenever required.

A typical lambda expression looks like :

[] (int x, int y) { return x + y }

Syntax of Lambda Expressions

The following are the structural elements of a lambda expression’s syntax:
 
a) Capture Clause
b) Parameter List
c) Mutable Specification
d) Exception Specification
e) Return Type
f) Lambda Body

a) Capture Clause ( [ ] )

 The Capture Clause is the first element of a lambda expression. As mentioned above, a lambda expression is like an anonymous function. Now, if a lambda expression has to access local variables and function parameters, they must be captured. This is where the Capture Clause plays its role. You have to decide whether the lambda captures variables by reference or by value.

The variables that have the & (ampersand) prefix are accessed by reference, while those that do not have it are accessed by value. As you might guess, in case of variables accessed by reference, the value of the original variable gets affected by the changes made to the captured variable.

On the other hand, in case of variables accessed by value, no change is reflected in the original variable. Please note that it is necessary to use mutable for modifying the values of copies of variables inside the lambda. If you do not use the mutable specification, you cannot modify even the copies of the variables inside the lambda. And under no condition can you modify the value of the original variables in the case of capture by value. A detailed explanation of this situation is described in the example given in the “Mutable Specification” section below.

If the capture clause is empty i.e. [], it means that the body of the lambda accesses no variables in the enclosing scope


For example,

We have two variables num_even and num_odd.

If we want both variables to be captured by value, we would write :

 
[num_even, num_odd]


If both of them are to be captured by reference,

[&num_even, &num_odd]


In case we want one of these to be captured by value and another by reference,

[num_even, &num_odd] or [&num_even, num_odd]


In the last case i.e. [&num_even, num_odd], the value of the original num_even variable can be modified by the lambda expression (due to being accessed by reference) while the original num_odd variable can’t be modified (since it is being accessed by value).

Default Capture Mode

  You can also use the default capture mode to capture unspecified variables either by value or by reference. To specify the default capture mode, you have to use & or = as the first element of the capture clause. If & is used, it implies that all the unspecified variables will be accessed by reference, while using = causes all the unspecified variables to be accessed by value.


For example, in the above example of num_even and num_odd, if we use [&] then it means that both num_even and num_odd will be accessed by reference.

[&, num_odd] causes num_odd to be captured by value and num_even (and other variables, if any) to be captured by reference.

Similarly, [=, &num_odd] causes num_odd to be captured by reference and num_even (and other variables, if any) to be captured by value.

There are some rules regarding the Capture Clause :

i) A variable or this cannot appear more than once in a capture clause.

For example,

[num_even, num_even] () {} is invalid

ii) If a Capture Clause uses the default capture mode using &, then no variable in the clause can use the form &variable.


For example, you cannot use

[&, &num_even] because & has been used in default capture mode. If you want to capture num_even by reference, you have the following two options :


[&] or [&num_even]
 

iii) Similarly, if a Capture Clause uses the default capture mode using =, then no variable in the clause can be explicitly declared by value.

For example, you cannot use

[=, num_even] because = has been used in default capture mode. If you want to capture num_even by value, you have the following two options :

[=] or [num_even]

 

b) Parameter List

    The parameter list of a lambda expression is the second element of a lambda and resembles the parameter list for a function. It can also contain another lambda expression as its argument.

For example,

(int i, int j) and (float x, float y) are valid parameter lists.

Please note that the use of the parameter list is optional. Hence, you can omit the empty parantheses i.e. () if

i) You do not pass arguments to the lambda expression, and
ii) It does not contain exception specification, return type or mutable.

c) Mutable Specification

    The mutable specification is the third element of a lambda expression and it enables the body of a lambda expression to modify variables that are captured by value. Without using mutable, we can only read these values. Remember, even if we use mutable, we can only modify the copied values of those variables. It cannot change the values of original variables (that exist ouside the lambda).


For example,

for_each (v.begin(), v.end(), [num_even, num_odd](int n) {    
        cout << n;
if (n % 2 == 0) {
cout << ” is even ” << endl;
++num_even;
}
else {
cout << ” is odd ” << endl;
++num_odd;
}
});

In the above example, the compiler will show an error saying that variables captured by value cannot be modified. If we use mutable, like the following example, we can modify these values.

for_each (v.begin(), v.end(), [num_even, num_odd](int n) mutable {    
        cout << n;
if (n % 2 == 0) {
cout << ” is even ” << endl;
++num_even;
}
else {
cout << ” is odd ” << endl;
++num_odd;
}
});


Now, the program will compile successfully and perform its task properly.

d) Exception Specification

    The throw() exception specification is the fourth element of the structure of a lambda expression and it can be used to indicate that the lambda expression does not throw any exceptions. However, if the lambda does throw an exception, an error is shown (in Visual C++) which says

‘function’ : function assumed not to throw an exception but does

For example, 

int main() 
{
  []() throw() { throw 5; }();
}

This above program shows the error mentioned above. Therefore, you must make sure that if a lambda expression uses throw(), it should not throw an exception in its body.

e) Return Type

   The return type of a lambda expression is automatically deduced. To explicitly specify the return type of a lambda, you must place -> followed by the type name (such as int, void) after the parameter list.
 
For example,
 

for_each (v.begin(), v.end(), [&num_even, &num_odd] (int n) ->void {    
        cout << n;
if (n % 2 == 0) {
cout << ” is even ” << endl;
++num_even;
}
else {
cout << ” is odd ” << endl;
++num_odd;
}
});


As you can see in the above example, “->void” (without quotes) has been used to specify the return type of the lambda as void.

You can omit the return type part of a lambda expression if the lambda body contains just one return statement or the statement does not return a value. If the lambda body contains one return statement, the compiler deduces the reduces type from the type of the return expression. Otherwise the compiler deduces the return type as void.

For example,
auto x = [] (int i){return i;};
In the above example, the return type is automatically deduced as int.

f) Lambda Body

  The body of a lambda expression is the last element of a lambda and it can contain anything that a function can contain. The body of a lambda expression can access the following types of variables :


i) Parameters
ii) Locally declared variables
iii) Class data members (when the lambda has been declared inside a class and this is captured)
iv) Any variable that has static storage duration – e.g. global variables
v) Variables captured from the enclosing scope

If a variable appears in the capture clause of a lambda expression, it is considered as being explicitly captured, otherwise it is implicitly captured. You can recall from the “Capture Clause” section that the variables that are not explicitly mentioned in the capture clause are captured using the default capture mode.

For example,

for_each (v.begin(), v.end(), [&, num_odd] (int n) mutable {    
        cout << n;
if (n % 2 == 0) {
cout << ” is even ” << endl;
++num_even;
}
else {
cout << ” is odd ” << endl;
++num_odd;
}
});

 
Here, num_odd has been explicitly captured by value and num_even has been captured implicitly by reference since the default capture mode is capture by reference.
 
The usage and significance of lambda expressions has been discussed in the next article.



You can
provide us your feedback in your comments.

(Visited 246 times, 1 visits today)

2 comments

Leave a Reply

Your email address will not be published. Required fields are marked *