Lambdas
Introduction
Sometimes we need a function that will be used in a very limited way. Lambdas allow us to write an expression that will be used as a function.This paradigm is known as functional programming. We had function objects ( functors ) that involved creating a class and an operator function insided it. Now we can create a function in a much more simplistic and concse way. The capture clause can specify how we can include variables from outside the function.
[=] (parameters) mutable throw() -> return-type {body} 1 2 3 4 5 6 1) capture clause 2) parameters 3) mutable 4) throw 5) return type 6) bodyA lambda expression translates to a function object with a class and the operator "()" in the class. It's a more convenient notation than creating a class or an actual function. It can be used inline or used as arguments. We can use the handy "auto" to declare the type that the lambda expression will return.
Capture Clause
1) capture clause This dictates the variables that we can access before the lambda function.
File: lambdas11.cpp
#include <algorithm> #include <array> #include <string> #include <functional> #include <iostream> using namespace std ; class Integer { public: int x1 ; Integer(int x1p) { x1 = x1p ; cout << "Object Created" << x1 << endl; } Integer(const Integer& param1) { x1 = param1.x1 ; cout << "Copy constructor: Object Created" << x1 << endl; } ~Integer() { cout << "Object Destroyed: " << x1 << endl; } }; int main() { int x1 = 10 ; int x2 = 10 ; //outside variable like x1 is not captured auto print1 = [](string str1){ cout << str1 << endl ; } ; print1( "Calling the lambda print function." ) ; //Trying to access the variable outside the //scope auto print2 = [](string str1){ cout << str1 << endl ; // cout << x1 << endl ; } ; print2( "Calling the lambda print2 function." ) ; auto print3 = [=](string str1){ cout << str1 << endl ; cout << x1 << endl ; //x1=5 ; Compiler error //x1 is read only } ; print3( "Calling the lambda print3 function." ) ; auto print4 = [&](string str1){ cout << str1 << endl ; x1=4 ; } ; print4( "Calling the lambda print4 function." ) ; cout << x1 << endl ; auto print5 = [x1](string str1){ cout << str1 << endl ; cout << x1 << endl ; //cout << x2 << endl ; //The "x2" is not accessible here. } ; print5( "Calling the lambda print5 function." ) ; //if ( 1 == 1 ) // return 1 ; Integer obj1( 10 ) ; //A copy is created inside the lambda //Copy constructor gets called. //When print6 goes out of scope then //the obj1 gets destroyed. auto print6 = [=](string str1){ cout << str1 << endl ; cout << obj1.x1 << endl ; //We cannot modify obj1 } ; print6( "Calling the lambda print6 function." ) ; //capture all the variables by reference //Copy constructor does not get called. cout << "-------------------" << endl ; auto print7 = [&](string str1){ cout << str1 << endl ; cout << obj1.x1 << endl ; } ; print7( "Calling the lambda print7 function." ) ; cout << "-------------------" << endl ; //Allowing access to a single variable/object //Another copy of obj1 gets created here. auto print8 = [obj1](string str1){ cout << str1 << endl ; cout << obj1.x1 << endl ; // Other variables are not captured // cout << x1 << endl ; } ; print8( "Calling the lambda print8 function." ) ; cout << "-------------------" << endl ; //Passing a single variable by reference auto print9 = [&obj1](string str1){ cout << str1 << endl ; //Allowed obj1.x1 = 100 ; cout << obj1.x1 << endl ; } ; print9( "Calling the lambda print9 function." ) ; //Changes are preserved after the invocation cout << obj1.x1 << endl ; cout << "-------------------" << endl ; cout << "-------------------" << endl ; cout << "mix and match example" << endl ; //we can mix and match auto print10 = [&obj1, x1](string str1){ cout << str1 << endl ; cout << x1 << endl ; cout << obj1.x1 << endl ; } ; print10( "Calling the lambda print10 function." ) ; cout << "-------------------" << endl ; }
In the above file we see the statement: auto print1 = [](string str1){ cout << str1 << endl ; } ; print1( "Calling the lambda print1 function." ) ; We are using the handy "auto" to define the type for the lambda expression. As we will see later the lambda is essentially a functor object. We are not trying to access the variables defined in the scope above the lambda definition. auto print2 = [](string str1){ cout << str1 << endl ; // cout << x1 << endl ; } ; print2( "Calling the lambda print2 function." ) ; The capture close is empty ( "[]" ). So if we try to access "x1" then we get a compiler error. auto print3 = [=](string str1){ cout << str1 << endl ; cout << x1 << endl ; //x1=5 ; //x1 is read only } ; print3( "Calling the lambda print3 function." ) ; Here we place the equal sign in the capture clause "[=]". This means we have access to the other variables by value. We cannot change the values as the "x1" is considered read only. If we do want to change the value then we need to pass "x1" by reference. auto print4 = [&](string str1){ cout << str1 << endl ; x1=4 ; } ; print4( "Calling the lambda print4 function." ) ; cout << x1 << endl ; We see in the output that the value of "x1" change was retained. Calling the lambda print4 function. 4 auto print5 = [x1](string str1){ cout << str1 << endl ; cout << x1 << endl ; //cout << x2 << endl ; //The "x2" is not accessible here. } ; print5( "Calling the lambda print5 function." ) ; We can choose to make only certain variables accessible to the lambda expression. In the above we are making "x1" accessible but denying access to any other variables such as "x2". Integer obj1( 10 ) ; //A copy is created inside the lambda //Copy constructor gets called. //When print6 goes out of scope then //the obj1 gets destroyed. auto print6 = [=](string str1){ cout << str1 << endl ; cout << obj1.x1 << endl ; //We cannot modify obj1 } ; We can also choose to capture objects.In the above we are capturing the object "obj1" and another copy gets created in the lambda expression even though the object name stays the same which is "obj1". We can see that because the "copy constructor" gets called when we invoke this lambda object. The same name strategy works fine because we are not allowed to modify "obj1". Of course in order to modify "obj1" we need to use the reference clause. //capture all the variables by reference //Copy constructor does not get called. cout << "-------------------" << endl ; auto print7 = [&](string str1){ cout << str1 << endl ; cout << obj1.x1 << endl ; obj1.x1 = 5 ; } ; print7( "Calling the lambda print7 function." ) ; //Allowing access to a single variable/object //Another copy of obj1 gets created here. auto print8 = [obj1](string str1){ cout << str1 << endl ; cout << obj1.x1 << endl ; // Other variables are not captured // cout << x1 << endl ; } ; We can choose to allow access to just a single object as the above snippet shows. We could also place more variables in the capture clause separated by commas. auto print9 = [obj1, x1](string str1){ cout << str1 << endl ; cout << obj1.x1 << endl ; // Other variables are not captured cout << x1 << endl ; } ; print9( "Calling the lambda print9 function." ) ; Passing the object by reference. auto print10 = [&obj1](string str1){ We can choose to make some variable as pass by reference and others pass by value. //we can mix and match auto print11 = [&obj1, x1](string str1){ cout << str1 << endl ; cout << x1 << endl ; cout << obj1.x1 << endl ; } ; print11( "Calling the lambda print11 function." ) ; cout << "-------------------" << endl ; In the below we are stating that "obj1" is passed by reference and all other parameters are passed by value. auto print12 = [&obj1, =](string str1){ cout << str1 << endl ; cout << x1 << endl ; x1 = 13 ; obj1.x1 = 12 ; cout << obj1.x1 << endl ; } ; print12( "Calling the lambda print12 function." ) ; cout << "-------------------" << endl ;
STL
We shall examine how lambdas are used in STL.File: lambdas12.cpp
#include <algorithm> #include <array> #include <string> #include <functional> #include <iostream> using namespace std ; class customSort1 { public: bool operator()(int const& s1, int const& s2) { return s1 < s2 ; } }; struct Add { int operator()(int a, int b) const { cout << "Inside Add operator." << endl ; return a + b; } }; int main() { //Using lambda in stl sort algorithm std::array<int, 10> arr1{5, 7, 4, 2, 8, 6, 1, 9, 0, 3}; auto print1 = [&arr1](const string& rem) { for (auto element : arr1) std::cout << element << ' '; std::cout << ": " << rem << '\n'; }; sort(arr1.begin(), arr1.end()); print1("sorted with the default operator<"); auto customSort = [] (int const& s1, int const& s2) -> bool { return s1 > s2 ; }; sort(arr1.begin(), arr1.end(), customSort); print1("sorted with the customSort "); //Inline sort(arr1.begin(), arr1.end(), [] (int const& s1, int const& s2) -> bool { return s1 > s2 ; }); print1("sorted with the inline lambds "); //Using the built in functor "greater" //Signature of the greater is //template <class T> struct greater { // bool operator()(const T& lhs, const T& rhs) const; //}; sort(arr1.begin(), arr1.end(), greater<int>() ); //Use a custom functor object customSort1 customSort1Object ; sort(arr1.begin(), arr1.end(), customSort1Object ); print1("Sorted with the functor object."); //Return statements can be implicit or //explicit. //Example of implicit return auto add = [] (int a, int b) { // always returns an 'int' return a + b; }; cout << "add called:" << add(4, 5) << endl ; //Explicit return statement auto add1 = [] (int a, int b ) -> double { return a + b ; }; cout << "add1 called:" << add1(4, 6) << endl ; //inline example // initialize vector of integers vector<int> nums = {1, 2, 3, 4, 5, 8, 10, 12}; int even_count = count_if(nums.begin(), nums.end(), [](int num) { return num % 2 == 0; } ); cout << "There are " << even_count << " even numbers." << endl ; //Without using auto //A lambda is a functor object. function<void(int, int)> add2 = [] (int a, int b) { cout << "Sum: " << a + b << endl ; }; add2(1, 2); Add add3Object ; //Assign a class object to a function object function<void(int, int)> add3 = add3Object ; add3( 10, 20 ) ; }
We have the concept of a function that can be defined with it's body. Then came along "functors". This is a class with an operator "()" such as: bool operator()(int const& s1, int const& s2) { return s1 < s2 ; } that can then be called directly by creating an object of the class and then just doing something like: obj1( 4, 5 ) ; The next language enhancement to this approach is lambdas. We have an expression that is like a function but is actually a functor. auto print1 = [&arr1](const string& messg) { for (auto element : arr1) std::cout << element << ' '; std::cout << ": " << messg << '\n'; }; In the above snippet we define a lambda "print1" that passes the "arr1" object and takes a string as an argument. We are basically printing out the elements of the "arr1" object. We first sort using the std::sort function: sort(arr1.begin(), arr1.end()); without any custom comparators. This sorts the container in an ascending order. auto customSort = [] (int const& s1, int const& s2) -> bool { return s1 > s2 ; }; sort(arr1.begin(), arr1.end(), customSort); In the above we define a lambda object "customSort" that takes 2 integer arguments and returns a boolean value. This sorts the container in a descending order. The signature for "sort" specifies the third argument to a functor that takes 2 arguments and returns a boolean value. Notice that we have explicitly specified the return type with the "-> bool" part of the expression. sort(arr1.begin(), arr1.end(), [] (int const& s1, int const& s2) -> bool { return s1 > s2 ; }); print1("sorted with the inline lambds "); We do not have to have a name for the lambda expression and can embed the expression in the function call as shown above. sort(arr1.begin(), arr1.end(), greater() ); In this call we are using the built in functor "greater" from the " " header file. The signature is as: template struct greater { bool operator()(const T& lhs, const T& rhs) const; }; As we can see we can use lambda expressions and functors interchangeably. class customSort1 { public: bool operator()(int const& s1, int const& s2) { return s1 < s2 ; } }; //Use a custom functor object customSort1 customSort1Object ; sort(arr1.begin(), arr1.end(), customSort1Object ); print1("Sorted with the functor object."); In the above we have our own functor custom class. We create an object of that and pass it to the sort function. //Explicit return statement auto add1 = [] (int a, int b ) -> double { return a + b ; }; cout << "add1 called:" << add1(4, 6) << endl ; The above snippet shows how to specify the type that the lambda expression will return. The type is specified just before the body of the lambda expression. //Without using auto //A lambda is a functor object. function add2 = [] (int a, int b) { cout << "Sum: " << a + b << endl ; }; add2(1, 2); The above snippet shows an alternative to using "auto" to declare the type for lambda. Since a lambda object is a functor, we can use the stl function to specify that the functor takes 2 integer arguments and returns null. We can then call the functor object just as we would call a lambda object.
Dangling Reference
We need to be careful that if we capture the variables by reference then that variable does not go out of scope when we use the lambda object.File: dangle1.cpp
#include <iostream> #include <functional> std::function<int()> make_adder(int x1) { return [&x1]() { return x1 + 1; }; } int main() { auto add_one = make_adder(5); std::cout << add_one() << std::endl; // This will likely cause a crash or undefined behavior return 0; }
From the above file we have: std::functionmake_adder(int x1) { return [&x1]() { return x1 + 1; }; } This is clearly wrong. The "x1" is a local variable to the function "make_adder" . When we use it: auto add_one = make_adder(5); std::cout << add_one() << std::endl; // This will likely cause a crash or undefined behavior When the lambda "add_one" is used in the cout statement the return value is referencing the local variable "x1" that has gone out of scope. The behavior at this point is undefined. This is the same problem that we encounter with a local reference such as: int& function1() { int x1 = 5 ; return x1 ; } When the "function1()" is called from some other place in the code then the value being to referred to has gone out of scope.
Mutable
We can change the value of the parameter in the capture clause in the lambda function but that change is not refected in the code calling the lambda function. It's as if the parameter is local to the lambda expression.File: lambdas13.cpp
Output: $./a.exe In main(): a1 = 1, b1 = 1 In add_one(): a1 = 2, b1 = 2 In main() After calling the lambda function: a1 = 1, b1 = 2We are passing 2 variables in the parameters clause. The "a1" is passed by value and the "b1" is passed by reference. We are
Invocation
File: lambdas14.cpp
#include <algorithm> #include <array> #include <string> #include <functional> #include <iostream> using namespace std ; int main() { int num1 = 1; int num2 = 2; auto lambda1Obj = [] (int a, int b) { return a + b; } ; cout << "lambda1Obj invoked: " << lambda1Obj( 5,6) << endl ; //invoked right away auto sum = [] (int a, int b) { return a + b; } (num1, num2); cout << "The sum of " << num1 << " and " << num2 << " is " << sum << endl ; }
Output: $ g++ lambdas14.cpp ; ./a.exe lambda1Obj invoked11 The sum of 1 and 2 is 3 We have seen how the lambda expression can be evaluated. We can invoke it inline or we can assign it to a lambda object and then call the lambda as shown in the code above. auto lambda1Obj = [] (int a, int b) { return a + b; } ; cout << "lambda1Obj invoked: " << lambda1Obj( 5,6) << endl ; The above snippet shows the lambda object being created. We can also have the expression executed at the point of definition with the below snippet. //invoked right away auto sum = [] (int a, int b) { return a + b; } (num1, num2); cout << "The sum of " << num1 << " and " << num2 << " is " << sum << endl ; Here the variable "sum" is of type integer. The lambda expression gets executed with arguments "num1" and "num2" .
Exceptions
File: except1.cpp
#include <iostream> #include <stdexcept> using namespace std ; int main() { auto divide = [](int a, int b) -> int { if (b == 0) { throw runtime_error("Division by zero!"); } return a / b; }; try { int result = divide(10, 0); cout << "Result: " << result << endl; } catch (const runtime_error& e) { cerr << "Error: " << e.what() << endl; } auto divide1 = [](int a, int b) noexcept -> int { if (b == 0) { cout << "Divisor is zero. Can't divide." << endl ; return 0; } return a / b; }; auto divide2 = [](int a, int b) noexcept -> int { if (b == 0) { cout << "Divisor is zero. Can't divide." << endl ; throw runtime_error("Division by zero!"); } return a / b; }; }
Output: $ rm a.exe ; g++ except1.cpp ; ./a.exe except1.cpp: In lambda function: except1.cpp:41:14: warning: ‘throw’ will always call ‘terminate’ [-Wterminate] 41 | throw runtime_error("Division by zero!"); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Error: Division by zero! We can have a lambda expression throw an exception. The below snippet shows how this can be done. auto divide = [](int a, int b) -> int { if (b == 0) { throw runtime_error("Division by zero!"); } return a / b; }; try { int result = divide(10, 0); cout << "Result: " << result << endl; } catch (const runtime_error& e) { cerr << "Error: " << e.what() << endl; } The lambda expression checks if the divisor is 0 and if so throws a "runbtime_error" exception which is caught later on in a try block upon the execution of the lambda object "divide" . auto divide1 = [](int a, int b) noexcept -> int { if (b == 0) { cout << "Divisor is zero. Can't divide." << endl ; return 0; } return a / b; }; We can also have exception specification as shown above. The "noexcept" is stating that this lambda expression does not throw an exception. auto divide2 = [](int a, int b) noexcept -> int { if (b == 0) { cout << "Divisor is zero. Can't divide." << endl ; throw runtime_error("Division by zero!"); } return a / b; }; In the above we have the "noexcept" specification but the lambda expression throws an exception. In this case we get a compiler warning.