Chapter 2

Working with Lambda Expressions

IN THIS CHAPTER

check Defining why you need lambda expressions

check Understanding the parts of a lambda expression

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

Remember You don’t have to type the source code for this chapter manually. In fact, using the downloadable source is a lot easier. You can find the source for this chapter in the \CPP_AIO4\BookIII\Chapter02 folder of the downloadable source. See the Introduction for details on how to find this book’s source files.

Creating More Readable and Concise C++ Code

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:

  • Function literal
  • Lambda abstraction
  • Lambda expression

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.

Remember From a computer language perspective, anonymous functions often enable you to clear the code of a plethora of one- or two-line function declarations. Some languages even require the use of anonymous functions for tasks such as binding events to callbacks or instantiating a function for particular values. However, this isn’t the case in C++; everything you can do with a lambda expression, you can also do with a named function. When considering lambda expressions in C++, you gain these advantages:

  • Greater efficiency: The compiler doesn’t have to create a stack frame for lambda expressions, so less underlying machine code is generated.
  • Better readability: Locating a one- or two-line named function consumes developer time and makes the code less readable because you don’t see it in context.
  • Fewer errors: By making a function concise and targeted, it’s possible to reduce coding errors because the function is also more understandable.
  • Reducing frankenfunctions: Using lambda expressions can help rid your code of those named functions that try to do too much using too many different styles and not accomplishing a great deal except confusing the developers who look at it. When thinking about frankenfunctions, think about those named functions that are put together from bits and pieces of one- or two-line functions and whose actions are differentiated using if…then or switch statements.

Tip The most important concept to take away from this section is that lambda expressions don’t replace named functions; they simply provide an alternative style that you can use to make your code better. Given that they represent a style and not a coding mandate, you need to work with them for a while to define a comfort level that makes sense in the applications you write.

Defining the Essential Lambda Expression

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.

Defining the parts 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

captures

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 https://en.cppreference.com/w/cpp/language/lambda#Lambda_capture offers additional details.

tparams

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 https://en.cppreference.com/w/cpp/language/templates. You may supply an optional requires clause to place constraints on the templated functionality.

params

Various

Defines the parameters passed into the lambda expression for processing. Starting with C++ 14, you can use default arguments and the auto keyword.

specifiers

Various

Modifies the manner in which the code interacts with captured external variables. The following keywords are available:

mutable: Allows modification of the variables and objects, and the calling of object non-const members.

constexpr (C++ 17 and above): Specifies that the function call operator is a constexpr (see the “Creating constant expressions” section of Chapter 1 of this minibook for details).

consteval (C++20 and above): Specifies that the function call operator is an immediate function (see the “Defining an immediate function” section of Chapter 3 of this minibook for details).

exception

Various

Creates a dynamic exception specification (see the “Specifying that the lambda expression throws exceptions” section, later in this chapter) or defines a noexcept specifier (C++ 11 and above).

attr

11

Adds attributes to the function for implementation specifics, such as working on the GNU or IBM platforms. The discussion at https://en.cppreference.com/w/cpp/language/attributes provides additional details on using attributes.

ret

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.

requires

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 tparams. The discussion at https://en.cppreference.com/w/cpp/language/constraints provides additional details.

body

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.

Relying on computer detection of return type

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.

Warning Unfortunately, as described in the “Understanding the Role of auto” section of Chapter 1 of this minibook, the automatic detection (deduction) on the part of the compiler doesn’t always work precisely as planned. In addition, depending on how you define the function input to a function, you can get some strange results. Consequently, you always need to exercise care in the use of this feature. The ReturnDeduction example, shown in Listing 2-1, demonstrates how you can obtain different results based on whether you specify a return type or allow the computer to deduce it for you. (This example uses the same DemangleIt() function described in the “Understanding the Role of auto” section of Chapter 1 of this minibook.)

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;
}

Remember The input argument for the functions in this example is function<>. When using function<>, you specify a return type, even if the return type is void, and any input types, or empty parentheses, (), when the function doesn’t need one. Part of the problem with both ShowType() and ShowChar() is that the function<> declaration doesn’t allow use of auto, so you get whatever type you define, as you see later in the example.

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.

Tip You probably think that one or more of these lambda expressions will fail, especially given the input values used in ShowType() and ShowChar(). However, they all do work, as shown in the somewhat surprising output here:

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.

Using the auto keyword with lambda expressions

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

Tip By giving up control over the form of the input to ShowData(), you also preserve the types of the various inputs. Each of the outputs is now of the correct type, and the char output (last) actually appears as a character rather than a number. However, there isn’t a best solution — only the solution that works to meet your specific requirements. You therefore need to keep the function<> method described in the previous section in mind.

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.

Using lambda expressions as macros

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

Developing with Lambda Expressions

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.

Using lambda expressions with classes and structures

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

Working with the capture clause

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

Remember The internal values of Total have changed, but the external value of Total remains 0. You can also change this behavior by changing [Total = 5] to read [&Total]. Note that you can’t initialize Total if you also plan to access it by reference, so [&Total = 5] won’t work. Here’s the new output:

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.

Sorting data using a lambda expression

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

Specifying that the lambda expression throws exceptions

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.

Remember The MultiTask example (refer to Listing 2-5) throws an exception when the caller doesn’t provide an acceptable input of the correct type. Throwing an exception is still perfectly acceptable, and when you call outside functions from your lambda expression, these functions can throw exceptions, too. However, sometimes you really don’t want the exception to occur because the unexpected situation is expected. To get past this problem, you can use noexcept() to disregard the exception, like this:

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.