Chapter 2
IN THIS CHAPTER
Defining why you need lambda expressions
Understanding the parts of a lambda expression
Performing practical tasks with lambda expressions
The “Using Lambda Expressions for Implementation” section of Chapter 1 of this minibook offers a brief overview of lambda expressions as they apply to transforms. However, lambda expressions can do considerably more than you discover in that section’s example. Using lambda expressions isn’t required to write good C++ code, but they can make your C++ code better and allow for certain optimizations in some cases. This chapter discusses in more detail how and when you use lambda expressions.
This chapter also describes the parts of a lambda expression. You may not ever use everything that a lambda expression has to offer, but it’s good to know what’s available. You might find that you can make your code even shorter and easier to understand by creating just the right type of lambda expression.
Finally, the chapter shows some examples of how to use lambda expressions for practical purposes such as sorting data. It also helps you understand some development nuances, such as throwing an exception when necessary. Even though this section isn’t comprehensive, it provides enough basics for you to know how to use lambda expressions effectively. Chapter 3 of this minibook addresses some additional advanced examples.
The lambda expression begins with the anonymous function, which actually existed before electronic computers. Alonzo Church (https://history-computer.com/ModernComputer/thinkers/Church.html
) created the idea of an anonymous function in 1936, which is part of lambda calculus. One of the first computer languages to use anonymous functions was LISP, in 1958. Here are the kinds of anonymous functions you commonly run across in computer science:
Of course, the target of this chapter is the lambda expression, but it pays to know about the other forms of anonymous function as well. All forms of anonymous function share one trait: They aren’t associated with any sort of identifier. In other words, you typically use them for short, concise calculations that an application may need to perform only once. From a human perspective, using a lambda expression makes code simpler to understand by placing the function inline rather than in a separate block of code. This technique could lead to spaghetti code of the worst sort when used inappropriately, so some restraint is required on the part of the developer.
if…then
or switch
statements.Because lambda expressions rely on math, rather than on coding technique, they have a form that is common across computer languages. You can’t make a direct replacement of a lambda expression written in one language into another, but understanding the lambda expressions in both languages is easier because they have a similar form. The following sections discuss the C++ specific form of a lambda expression.
A lambda expression can take a number of forms. You saw one of those forms in the “Using Lambda Expressions for Implementation” section of Chapter 1 of this minibook. However, it’s time to look at the full definition of the lambda expression:
[ captures ] <tparams> ( params ) specifiers exception
attr -> ret requires { body }
Not all of these elements are needed in every case, and some are available only in some versions of C++. With these limitations in mind, Table 2-1 provides a description of each of the elements (with a blank version column indicating that the element is available in all current versions).
TABLE 2-1 Elements of a Lambda Expression
Element |
Minimum C++ Version |
Description |
---|---|---|
|
Various |
Specifies how external variables are captured for use by the lambda expression. The default is to capture variables by reference. However, you can also create a copy of the variable. You can define a single policy for all variables or provide policies for individual variables. Starting with C++ 14, you can also provide a variable initializer in case the variable hasn’t been initialized. The reference at |
|
20 |
Provides a templated method of defining variable types. You use it to provide type names to the parameters of a generic lambda. This entry works much like the templates described at |
|
Various |
Defines the parameters passed into the lambda expression for processing. Starting with C++ 14, you can use default arguments and the |
|
Various |
Modifies the manner in which the code interacts with captured external variables. The following keywords are available:
|
|
Various |
Creates a dynamic exception specification (see the “Specifying that the lambda expression throws exceptions” section, later in this chapter) or defines a |
|
11 |
Adds attributes to the function for implementation specifics, such as working on the GNU or IBM platforms. The discussion at |
|
Indicates the return type of the lambda expression. If you don’t include this element, the compiler will deduce the return type based on the code you provide. | |
|
20 |
Defines requirements for template arguments that make it easier to choose the correct function overloads and template arguments. You use this element as an optional addition to |
|
Contains the function body — that is, the code that will actually execute. |
Some common patterns are used to create lambda expressions so that you don’t have to rely on the full version shown earlier in this section. Here are the most common forms:
[ captures ] ( params ) -> ret { body }
: Defines a const
lambda, which is the most common form. All the captured variables are const
in this case, and you can’t modify them.[ captures ] ( params ) { body }
: Specifies a const
lambda in which the return type is deduced by the compiler. The compiler uses the function’s return
statement as a basis for making the deduction.[ captures ] { body }
: Creates a lambda expression that requires no inputs. You can’t use this form if the lambda expression makes use of the constexpr
, mutable
, exception specification, attributes, or trailing return type features.The automatic detection feature of lambda expressions works much like the auto
keyword for other types of declarations. In most cases, the automatic detection feature works fine because it relies on the most common or default type for the output.
LISTING 2-1: Deciding Between a Deduced or Specific Return Type
#include <iostream>
#include <typeinfo>
#include <memory>
#include <cxxabi.h>
using namespace std;
string DemangleIt(const char* Mangled) {
int Status;
unique_ptr<char[], void(*)(void*)> Result(
abi::__cxa_demangle(Mangled, 0, 0, &Status), free);
return Result.get() ? string(Result.get()) : "Error";
}
void ShowType(function<float(double)> lambda) {
cout << "Input has a value of: " << lambda(2.6) << endl;
cout << "Input has type of: " <<
DemangleIt(typeid(lambda(2.6)).name()) << endl;
}
void ShowChar(function<char(int)> lambda){
cout << "Input has a value of: " << lambda(7) << endl;
}
int main() {
ShowType([](int x) -> int {return int(x * x);});
ShowType([](double x) -> int {return int(x * x);});
ShowType([](double x) -> double {return x * x;});
ShowType([](double x) {return float(x * x);});
ShowType([](double x) {return x > 2 ? true : false;});
ShowType([](int x) -> char {return char(x * 10);});
ShowChar([](int x) -> char {return char(x * 10);});
return 0;
}
The ShowType()
and ShowChar()
functions both show the value of lambda()
when you provide a specific input value to the lambda expression. The ShowType()
function also outputs the type of the value output by the function, and you’ll see the importance of this output in a moment.
The lambda functions are of the two const
types described in the “Defining the parts of a lambda expression” section, earlier in this chapter. Some specify a return type; others don’t. Note that the first two lambda expressions both provide an int
output, but one takes an int
as input and the other takes a double
. Playing with input and output types like this can help you understand the effects of decisions that you make when using lambda expressions. Note that the last three lambda expressions don’t actually return a numeric type. The first of these returns a bool
and the last two return char
.
Input has a value of: 4
Input has type of: float
Input has a value of: 6
Input has type of: float
Input has a value of: 6.76
Input has type of: float
Input has a value of: 6.76
Input has type of: float
Input has a value of: 1
Input has type of: float
Input has a value of: 20
Input has type of: float
Input has a value of: F
The compiler seems adept at making the lambda expressions work even when they really shouldn’t. For example, the first lambda expression accepts an int
as input and produces an int
as output, so the input is truncated, which results in an output of 4
. The second lambda expression truncates the output as an int
, so now you see 6
from what should be the same calculation, which should actually produce a value of 6.76
, as shown in the next two outputs. The bool
output is a value of 1
and the char output is a value 20
, neither of which reflects their true types. However, the really odd thing is that the type of all these outputs is float
(the default as explained in the next section); it doesn’t matter what the lambda expression actually provided as output. The point is that you need to exercise care in the construction of both the lambda expression and the function that receives it to obtain the desired result.
The auto
keyword can save you a great deal of pain when working with lambda expressions, plus it can help you avoid some common problems with getting the result you want. The UseAuto
example, shown in Listing 2-2, is a reworking of the example in Listing 2-1, shown earlier. However, even though the example works in a similar manner, the output is different because of the use of auto
.
LISTING 2-2: Performing Tasks Using auto
#include <iostream>
#include <typeinfo>
#include <memory>
#include <cxxabi.h>
using namespace std;
string DemangleIt(const char* Mangled) {
int Status;
unique_ptr<char[], void(*)(void*)> Result(
abi::__cxa_demangle(Mangled, 0, 0, &Status), free);
return Result.get() ? string(Result.get()) : "Error";
}
void ShowData(auto lambda){
cout << "Input has a value of: " << lambda(3.6) << endl;
cout << "Input has type of: " <<
DemangleIt(typeid(lambda(3.6)).name()) << endl;
}
int main() {
ShowData([](int x) -> int {return int(x * x);});
ShowData([](double x) -> int {return int(x * x);});
ShowData([](double x) -> double {return x * x;});
ShowData([](double x) {return float(x * x);});
ShowData([](double x) {return x > 2 ? true : false;});
ShowData([](double x) -> char {return char(x * 10);});
return 0;
}
When you run this example, you see that the auto keyword enables you to obtain results specific to the input. Here is what you see in this case (which you can compare to the output in the previous section):
Input has a value of: 9
Input has type of: int
Input has a value of: 12
Input has type of: int
Input has a value of: 12.96
Input has type of: double
Input has a value of: 12.96
Input has type of: float
Input has a value of: 1
Input has type of: bool
Input has a value of: $
Input has type of: char
Lest you think that someone could pass anything to ShowData()
, you can try, but you won’t be successful. If you were to pass something like ShowData(14)
, the compiler would output an error message of ’lambda’ cannot be used as a function
. Even though you’re using auto
, the auto
is still expecting a function as input.
You can assign a lambda expression to a variable and then use the variable as a kind of macro. This technique can make it a lot easier to perform some repetitive tasks that seem to appear everywhere, but take little code. The CreateMacro
example, shown in Listing 2-3, demonstrates this approach.
LISTING 2-3: Creating a Macro
#include <iostream>
using namespace std;
int main(){
auto f = [](auto Input) {cout << Input << endl;};
f("Hello");
f(221);
f(true);
f(99 / 3);
f(char(65));
f(int(15/4));
return 0;
}
The code in this example creates a simple lambda expression that outputs the input expression, whatever it might be, to the screen. To make the macro work, you use auto
in two contexts, both as the type of the variable holding the macro and as the input. Here’s the output you see:
Hello
221
1
33
A
3
The previous sections of the chapter give you an idea of how lambda expressions work. In the following sections of the chapter, you see how to implement certain lambda expression techniques in a more advanced manner that you might use within application code.
You can use lambda expressions for a wide variety of tasks with both classes and structures. In most cases, the tasks have something to do with data manipulation, such as finding data elements or sorting items, but lambda expressions can also see use for various kinds of analysis. The LambdaForClass
example, shown in Listing 2-4, stores a list of AnimalEntry
entries in the Animals
list found in the StoreAnimals
class. The lambda expression that defines FindAnimals()
helps locate a particular animal type and display the exhibits holding those animals in the zoo.
LISTING 2-4: Interacting with Classes and Structures
#include <iostream>
#include <list>
#include <algorithm>
using namespace std;
struct AnimalEntry {
string Name;
int CageLocation;
};
class StoreAnimals {
public:
void FindAnimals(string Name);
list<AnimalEntry> Animals;
};
void StoreAnimals::FindAnimals(string FindName) {
for_each(Animals.begin(), Animals.end(),
[FindName](AnimalEntry ThisEntry) {
if (FindName == ThisEntry.Name)
cout << ThisEntry.CageLocation << endl;
}
);
}
int main() {
StoreAnimals Zoo;
Zoo.Animals.push_back (AnimalEntry{"Hippo", 300});
Zoo.Animals.push_back (AnimalEntry{"Tiger", 301});
Zoo.Animals.push_back (AnimalEntry{"Tiger", 302});
Zoo.Animals.push_back (AnimalEntry{"Zebra", 303});
cout << "Finding hippo cages." << endl;
Zoo.FindAnimals("Hippo");
cout << "Finding tiger cages." << endl;
Zoo.FindAnimals("Tiger");
return 0;
}
An interesting part of this example is the use of a for_each()
to iterate the entries in the Animals
list. Even though this example iterates the entire list, you can also limit the search scope to specific records by providing a different beginning and ending point within the list.
This example also uses a simple capture, FindName
, to obtain the name of the animal to locate. The next section of the chapter provides additional details on how captures work, but it uses a different approach than this example does. The lambda expression must also accept an individual entry, ThisEntry
, of type AnimalEntry
, from the for_each()
.
The main()
code consists of creating a StoreAnimals
object, Zoo
, and populating the Animals list it contains with AnimalEntry
objects. The code can then call Zoo.FindAnimals()
to locate specific animals in the list. Here’s the output from this example:
Finding hippo cages.
300
Finding tiger cages.
301
302
You have many ways to use the capture clause, but one of the more interesting is to make your lambda expression a little more flexible. You can use the capture clause to help implement multiple behaviors by using a single lambda expression, as shown in the MultiTask
example in Listing 2-5.
LISTING 2-5: Performing Multiple Tasks by Using a Capture Clause
#include <iostream>
#include <typeinfo>
using namespace std;
struct AddVal_t {};
typedef AddVal_t AddVal;
struct SubVal_t {};
typedef SubVal_t SubVal;
int main() {
int Total = 0;
auto ChangeNum = [Total](auto Type, int Value) mutable {
if (is_same<decltype(Type), AddVal>::value) {
Total += Value;
return Total;
} else if (is_same<decltype(Type), SubVal>::value) {
Total -= Value;
return Total;
} else {
throw -1;
}
};
AddVal DoAdd;
SubVal DoSub;
cout << ChangeNum(DoAdd, 5) << endl;
cout << ChangeNum(DoAdd, 6) << endl;
cout << ChangeNum(DoSub, 4) << endl;
try {
cout << ChangeNum(5, 5) << endl;
} catch (int e) {
cout << "Error in Input!" << endl;
}
cout << Total << endl;
return 0;
}
This example is actually capable of doing a number of things, and you should experiment with it. For one thing, you begin with two structures, AddVal_t
and SubVal_t
, that are now empty but could be expanded to provide additional functionality. The code defines two types: AddVal
and SubVal
, based on these structures.
The lambda expression depends on an external variable, Total
, which is initialized to 0
. The ChangeNum()
declaration uses Total
as a capture, and you’ll see later in this section why that’s important. The two input arguments, Type
(defines what operation to perform) and Value
(defines the amount of change), work just like any other set of arguments. The mutable element specifies that the code can change Total
.
You could use phrases, numbers, or other methods of determining an action for this example, but the example uses types instead. If ChangeNum()
receives an input of the appropriate type, it will perform the appropriate action. Because of the way that this code is structured, the action can be type specific. The call to is_same()
determines whether the input type, Type
, is the same as a base type, such as AddVal
or SubVal
. After the types are verified, the code performs type-specific tasks. If the type isn’t present, ChangeNum()
throws an exception.
To use the lambda expression, the code must create variables of the correct type. Normally, you’d initialize the variables, DoAdd
and DoSub
, but because the example uses an empty structure, you don’t need to in this case. The code then calls ChangeNum()
using various operations and values, including one incorrect call. Note that the code also checks the value of Total
at the end. Here’s the output you should see:
5
11
7
Error in Input!
0
Even though the lambda expression has tracked Total
internally, it hasn’t changed the external value at all. So, you see the expected outputs for each call, but the actual value of Total
doesn’t change. Of course, you also see the error output for incorrect inputs.
The method of capture is important. For example, you can initialize the capture should you want to do so. Change [Total]
to read [Total = 5]
and then rerun the code. The outputs now look like this:
10
16
12
Error in Input!
0
5
11
7
Error in Input!
7
Now the external value of Total
reflects the manipulations of the lambda expression. Although writing even more complex lambda expressions than the one shown here is possible, you need to consider when you’ve reached the point where you should be using a standard function, rather than a lambda expression. Ideally, this example demonstrates the upper end of lambda expression complexity.
Although a computer can deal with data in any order, humans require order to make sense of the data. The standard sorting functions provided with C++ work well with data in standard format, such as a single-column list. However, after you start adding structures or classes, the data is much harder to sort without help. The SortList
example, shown in Listing 2-6, shows how to perform a single-column and a two-column sort on data formatted with a structure, Collect
, into a Collectables
list (a vector
, in this case).
LISTING 2-6: Performing Sorting Tasks
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Collect {
string Name;
int Height;
string Location;
};
int main() {
vector<Collect> Collectables;
Collectables.push_back ({"Statue", 40, "Basement"});
Collectables.push_back ({"Statue", 30, "Basement"});
Collectables.push_back ({"Mirror", 54, "1st Floor"});
Collectables.push_back ({"Statue", 33, "1st Floor"});
Collectables.push_back ({"Mirror", 33, "2nd Floor"});
Collectables.push_back ({"Chair", 44, "1st Floor"});
Collectables.push_back ({"Chair", 36, "2nd Floor"});
auto SortRule1 = [](Collect S1, Collect S2) {
return S1.Location < S2.Location;
};
auto SortRule2 = [](Collect S1, Collect S2) {
if (S1.Location != S2.Location)
return S1.Location < S2.Location;
return S1.Name < S2.Name;
};
sort(Collectables.begin(), Collectables.end(),
SortRule1);
cout << "One Column Sort" << endl;
for (auto s: Collectables)
cout << s.Name << "\t" << s.Height << "\t"
<< s.Location << endl;
sort(Collectables.begin(), Collectables.end(),
SortRule2);
cout << endl << "Two Column Sort" << endl;
for (auto s: Collectables)
cout << s.Name << "\t" << s.Height << "\t"
<< s.Location << endl;
return 0;
}
The example begins by creating a vector of items to sort. It then creates two sort rules. Both SortRule1
and SortRule2
perform comparisons and return a bool
value as to whether the comparison (the first item is less than the second item) is true. The difference is that SortRule2
performs the task on two columns of the list, so two levels of comparison are required. The code then calls sort()
to perform the list sorting and relies on a foreach loop to display the result, which appears here:
One Column Sort
Mirror 54 1st Floor
Statue 33 1st Floor
Chair 44 1st Floor
Mirror 33 2nd Floor
Chair 36 2nd Floor
Statue 40 Basement
Statue 30 Basement
Two Column Sort
Chair 44 1st Floor
Mirror 54 1st Floor
Statue 33 1st Floor
Chair 36 2nd Floor
Mirror 33 2nd Floor
Statue 40 Basement
Statue 30 Basement
Exceptions can be a difficult part of your code to implement properly because an exception indicates that something unexpected has happened and the caller needs to take action. Early versions of lambda expressions include a throw()
specification as part of the declaration, but the specification proved difficult to implement, and many programmers saw it as an awkward way to program. So, even though throw()
is still an optional part of the specification, you don’t generally see it used. In fact, you can’t use it in C++ 20 because it has been deprecated and removed.
cout << noexcept(ChangeNum(5,5)) << endl;
Instead of an exception, the code outputs the captured value of Total
, which is 0
. This is the operator form of noexcept()
. You also have access to a specifier version of noexcept()
that isn’t guaranteed to work with older versions of C++. It looks like this:
auto ChangeNum = [Total](auto Type, int Value) mutable
noexcept {…}
In this case, the inability to throw an exception affects the lambda expression as a whole, along with any functions that it calls. Of course, now you don’t know when exceptional conditions really do happen — the code simply outputs whatever answer it can, which is likely incorrect, when an unforeseen condition occurs.