Chapter 1
IN THIS CHAPTER
Understanding how functional programming works
Defining how functional programming differs
Implementing functional programming using lambda expressions
This minibook describes a different sort of C++ programming in the form of the functional programming paradigm. A paradigm is a framework that expresses a particular set of assumptions, relies on particular ways of thinking through problems, and uses particular methodologies to solve those problems. You’ll still use C++, but you use it in a manner that differs from the object-oriented programming (OOP) paradigms used in the previous minibook. Because many people are only now becoming aware of functional programming techniques, this chapter discusses how the functional and OOP paradigms differ.
The chapter also looks at some of the ways in which you change your programming style to use the functional programming paradigm. These style changes have some significant benefits when applied to certain kinds of development that rely heavily on math, perform various kinds of analysis, or work with technologies such as machine learning. You may not know it, but C++ is recommended as a language for both machine learning and deep learning in articles like the one at https://towardsdatascience.com/top-10-in-demand-programming-languages-to-learn-in-2020-4462eb7d8d3e
. However, making it work in these environments requires use of functional programming techniques.
And finally in this chapter, you discover how to implement functional programming strategies using lambda expressions. This is one of the simplest ways to achieve what you want with a minimum of disruption to your standard programming practices if you’re heavily involved in OOP. Later chapters delve more deeply into lambda expressions. This chapter just helps you get your feet wet.
Functional programming has somewhat different goals and approaches than other paradigms use. Goals define what the functional programming paradigm is trying to do in forging the approaches used by languages that support it. However, the goals don’t specify a particular implementation; doing that is within the purview of the individual languages.
In contrast to other paradigms, the functional programming paradigm doesn’t maintain state. The use of state enables you to track values between function calls. Other paradigms use state to produce variant results based on environment, such as determining the number of existing objects and doing something different when the number of objects is zero. As a result, calling a functional program function always produces the same result given a particular set of inputs, thereby making functional programs more predictable than those that support state.
Because functional programs don’t maintain state, the data they work with is also immutable, which means that you can’t change it. To change a variable’s value, you must create a new variable. Again, this makes functional programs more predictable than other approaches and makes functional programs easier to run on multiple processors.
Imperative programming, the kind of programming that most developers have done until now, is akin to an assembly line, where data moves through a series of steps in a specific order to produce a particular result. The process is fixed and rigid, and the person implementing the process must build a new assembly line every time an application requires a new result. Object-oriented programming (OOP) simply modularizes and hides the steps, but the underlying paradigm is the same. Even with modularization, OOP often doesn’t allow rearrangement of the object code in unanticipated ways because of the underlying interdependencies of the code.
Using pure functions creates a flexible environment in which code order depends on the underlying math. That math models a real-world environment, and as our understanding of that environment changes and evolves, the math model and functional code can change with it — without the usual problems of brittleness that cause imperative code to fail. Modifying functional code is faster and less error prone than other programming paradigms because the person implementing the change must understand only the math and doesn’t need to know how the underlying code works. In addition, learning how to create functional code can be faster as long as the person understands the math model and its relationship to the real world.
Functional programming also embraces a number of unique coding approaches, such as the capability to pass a function to another function as input. This capability enables you to change application behavior in a predictable manner that isn’t possible using other programming paradigms.
Many developers have come to see the benefits of functional programming. However, they also don’t want to give up the benefits of their existing language, so they use a language that mixes functional features with one of the other programming paradigms (as described in the “Considering Other Programming Paradigms” sidebar). For example, you can find functional programming features in languages such as C++, C#, and Java. When working with an impure language, you need to exercise care because your code won’t work in a purely functional manner, and the features that you might think will work in one way actually work in another. For example, you can’t pass a function to another function in some languages. The following sections help you understand why C++ is an impure functional language.
The basis of functional programming is lambda calculus (https://brilliant.org/wiki/lambda-calculus/
), which is actually a math abstraction. Every time you create and use a lambda function, you’re likely using functional programming techniques (in an impure way, at least). C++ supports lambda functions through the lambda expressions that later sections of this chapter explore.
In addition to using lambda functions, languages that implement the functional programming paradigm have some other features in common. Here is a quick overview of these features:
C++ is actually an extension of C. The original name of C++ was C with classes. So, theoretically, pure C++ is an OOP language. However, with the introduction of the Standard Library (see Book 5, Chapter 6 as well as Book 7 for more on the Standard Library), it becomes possible to add functionality to the language and make it more generic. The use of Standard Library enables you to use the functional programming paradigm in C++. However, even with Standard Library, you can’t turn what started out as a procedural language and became an OOP language into a functional programming language. The best you can hope to achieve is a language that supports a number of paradigms — some of them in a general way.
What occurs in C++ for the most part is that you rely on the Standard Library to hide the nonfunctional programming components. For example, you can use constants in your C++ code to create an immutable environment. You use templates to create functions that don’t rely on variables and therefore have no state. Using constants with methods can also help eliminate the problems with side effects. You see all these principles demonstrated as the chapter progresses. However, unlike a pure language, such as Haskell, these conventions aren’t enforced in C++, and humans will routinely find ways around them when programming needs dictate.
Passing a function to a C++ function can also prove difficult unless you rely on the Standard Library. For example, you can use a transform to interact with a range of values by passing the transform
a function. As part of the strategy of passing functions to other functions, you can rely on lambda expressions for simple needs. However, passing complex functions is possible as well. When working with complex functions, however, many developers encase them in a typedef
to make the code easier to read.
To create a pure function in C++, you must eliminate both state and side effects, which can be quite difficult. The process becomes especially difficult when working with external data, such as a file or a data stream. Obviously, a function that works with external data won’t produce the same output every time you call it, but you can still reduce the problems of both state and side effects.
Even the use of recursion in place of the usual for
or other looping mechanism can prove difficult in C++. In many cases, recursion relies on the use of mutable variables to track when the recursion should end. Careful use of various recursion strategies can make the use of mutable variables unnecessary, but doing so can be error prone and difficult (sometimes making the code hard to read).
Being able to change the content of a variable is problematic in C++. The memory location used by the variable is important. If the data in a particular memory location changes, the value of the variable pointing to that memory location changes as well. The concept of immutable data requires that specific memory locations remain untainted. To create immutable data in C++, you must use constant variables, as in
const double pi = 3.1415926;
The reason you need an immutable variable is that in a multiprocessing scenario, the value of the variable must be the same no matter which processor works with it. If x = 5
for one processor, it must equal 5
for all processors, and that value can never change. More important, the ability to change the value of a variable infers order, and functional programming techniques can’t rely on a specific order to accomplish their goals. Finally, immutable variables are reliable. You don’t have to worry about some bit of code, especially that from a hacker, modifying the values in your code because it seems like it might be a good idea. The following sections describe various forms of immutability in C++.
The Immutable
example, shown in Listing 1-1, demonstrates three techniques for creating immutable variables. In all three cases, you can rely on the variable’s value to remain consistent and also rely on the compiler to complain about any changes.
LISTING 1-1: Working with Constant Data
#include <iostream>
using namespace std;
struct Immutable{
int val{7};
};
int main() {
const int *test1 = new int(5);
*test1 = 10;
const int test2{6};
test2 = 11;
const Immutable test3;
test3.val = 12;
cout << *test1 << test2 << test3.val << endl;
return 0;
}
When you run this example, you see the following output in the Build Messages tab of the Code::Blocks compiler:
error: assignment of read-only location '* test1'
error: assignment of read-only variable 'test2'
error: assignment of member 'Immutable::val' in read-only
object
You can extend what you see here in other ways to make variables and their associated data immutable. Of course, now you have another problem — that of performing basic tasks, such as adding two numbers. To perform these tasks, you must begin using additional variables as containers like this:
const int sum = *test1 + test2;
It’s essential to understand that immutability comes in several levels when working with C++ classes and structures. The Immutable2
example, shown in Listing 1-2, shows two levels of immutability. The first occurs in the Immutable
structure, while the second occurs in main()
when attempting to make a change.
LISTING 1-2: Creating Immutable Structure Members
#include <iostream>
using namespace std;
struct Immutable {
int val{1};
void SayHi(string Name) const {
Name = "Smith";
val = 2;
cout << Name << val << endl;
}
void ChangeVal() {
val = 3;
cout << val << endl;
}
};
int main() {
const Immutable Test;
Test.ChangeVal();
Test.SayHi("Sam");
return 0;
}
Figure 1-1 shows the error messages you receive when you attempt to compile this application. The first error occurs because the SayHi()
method attempts to change val
internally. Notice that ChangeVal()
makes a similar change without error because it’s not a const
method (as created by adding const
after the method name and arguments to SayHi()
). The second error occurs because the ChangeVal()
call in main()
attempts to change val
through an external call.
FIGURE 1-1: Seeing errors generated as the result of immutability in a structure.
However, say that you want to allow internal changes to val
, yet continue to deny external changes to enforce functional programming. Adding mutable
to the val
declaration: mutable int val{1};
allows internal changes. Consequently, a new build will generate only the ChangeVal()
call error in main()
. If you comment out this call, you can see that the example will build and generate the following output: Smith2
. (The downloadable source provides these commented changes.)
Now the question is why it’s possible to change the Name
value in SayHi()
, if there aren’t supposed to be any changes. To make Name unchangeable, you must declare it as const
,
like this: void SayHi(const string Name) const
. So, now you know how to add immutability at various levels within structures and classes (which work the same as structures, in this case).
A constant expression, or constexpr
, is a special kind of function that you can compute at compile time rather than runtime. You create the code, just as you would any code, but the compiler converts the code into an output before the application even runs, which means that this is one form of immutability that also lacks state. Listing 1-3 shows the ConstantExpression
example that demonstrates how to create this kind of code. (This example won’t run with any version of C++ less than 11; the “Configuring Code::Blocks for smart pointers” sidebar in Book 1, Chapter 8 tells you how to perform this setup.)
LISTING 1-3: Creating Constant Expression Functions
#include <iostream>
using namespace std;
constexpr int factorial(int n) {
return n <= 1 ? 1 : (n * factorial(n - 1));
}
template<int n>
struct FactOut {
FactOut() {
cout << n << endl;
}
};
int main() {
// You can use a number if desired.
FactOut<15> Nothing1;
// Computed at compile time.
FactOut<factorial(4)> Nothing2;
// Computed at runtime.
cout << factorial(5) << endl;
return 0;
}
Here’s how all this works; main()
begins by providing an int
value of 15
to FactOut
. The next line supplies factorial(4)
as input to FactOut
, but FactOut
needs an int
value, so the compiler computes the value during compile time. At runtime, FactOut
still sees an int
value, but this time it’s a computed int
value. You can also use factorial()
as a standard function, but in this case, the application computes the value at runtime.
template<int n>
struct FactOut {
int val;
FactOut() {
cout << n << endl;
val = n;
}
};
In this case, val contains the computed value of n
. Consequently, you could use the variables you create like this: cout << Nothing1.val << endl;
. However, now you’re introducing a mutable variable again. To avoid problems, you’d need to declare Nothing1
as const FactOut<15> Nothing1;
.
Application state is a condition that occurs when the application performs tasks that modify global data. An application doesn’t have state when using functional programming. The lack of state has the positive effect of ensuring that any call to a function will produce the same results for a given input every time, regardless of when the application calls the function. However, the lack of state has a negative effect as well: The application now has no memory. When you think about state, think about the capability to remember what occurred in the past, which, in the case of an application, is stored as global data.
Avoiding state in any C++ application is nearly impossible. A problem area is any sort of file or stream data, which by nature changes. The FileLineCount
example, shown in Listing 1-4, demonstrates two techniques for determining the number of lines in a file named Temp.txt
. The first method, LineCount1()
, relies on state to track the current number of lines and the current character. The second method, LineCount2()
, doesn’t directly contain any sort of tracking; theoretically, it has no state.
LISTING 1-4: Avoiding the Use of State Directly
#include <iostream>
#include <fstream>
#include <algorithm>
using namespace std;
int LineCount1(string filename) {
int lineCount = 0;
char c = ' ';
ifstream thisFile(filename);
while (thisFile.get(c)) {
if (c == '\n')
lineCount++;
}
thisFile.close();
return lineCount;
}
int LineCount2(string filename) {
ifstream thisFile(filename);
return count(
istreambuf_iterator<char>(thisFile),
istreambuf_iterator<char>(), '\n');
}
int main() {
const string filename = "Temp.txt";
cout << LineCount1(filename) << endl;
cout << LineCount2(filename) << endl;
}
The term declaration has a number of meanings in computer science, and different people use the term in different ways at different times. For example, in the context of a language such as C++, a declaration is a language construct that defines the properties associated with an identifier. You see declarations used for defining all sorts of language constructs, such as types and enumerations. However, that’s not how you use the term declaration in a functional programming sense. The following sections describe side effects in terms of declarations and functions in the functional programming sense of the term declaration.
When making a declaration in functional programming, you’re telling the underlying language to do something. For example, consider the following statement:
The statement tells simply what to do, not how to do it. The declaration leaves the execution of the task to the party receiving it and infers that the party knows how to complete the task without additional aid. Most important, a declaration enables someone to perform the required task in multiple ways without ever changing the declaration. However, when using a function (or method) named MakeMeTea
(the identifier associated with the function), you might use the following sequence instead:
Declarations do suffer from another sort of inflexibility, however, in that they don’t allow for interpretation. When making a declarative statement (“Make me a cup of tea!”), you can be sure that the recipient will bring a cup of tea and not a cup of coffee instead. However, when creating a function, you can add conditions that rely on state to affect output. For example, you might add a step to the function that checks the time of day. If it’s evening, the recipient might return coffee instead of tea, knowing that the requestor always drinks coffee in the evening based on the steps in the function. A function therefore offers flexibility in its capability to interpret conditions based on state and provide an alternative output.
Declarations are quite strict with regard to input. The example declaration says that a cup of tea is needed, not a pot or a mug of tea. The MakeMeTea
function, however, can adapt to allow variable inputs, which further changes its behavior. You can allow two inputs, one called size
and the other beverage
. The size
input can default to cup
and the beverage
input can default to tea
, but you can still change the procedure’s behavior by providing either or both inputs. The identifier, MakeMeTea
, doesn’t indicate anything other than the procedure’s name. You can just as easily call it MyBeverageMaker
.
The second hardest issue is the loss of control. The language, not the developer, decides how to perform tasks. Yet, you sometimes see functional code where the developer tries to write it as a function, usually producing a less-than-desirable result (when the code runs at all).
An essential difference between functions and declarations is that functions don’t return a value in the same manner as declarations do. The previous paragraphs present a function that seems to provide the same result as the associated declaration, but the two aren’t the same. The declaration “Make me a cup of tea!” has only one output: the cup of tea. The function has a side effect instead of a value. After making a cup of tea, the function indicates that the recipient of the request should take the cup of tea to the requestor. However, the function must successfully conclude for this event to occur. The function isn’t returning the tea; the recipient of the request is performing that task. Consequently, the function isn’t returning a value.
Side effects also occur in data. When you pass a variable to a function, the expectation in functional programming is that the variable’s data will remain untouched — immutable. A side effect occurs when the function modifies the variable data so that upon return from the function call, the variable changes in some manner.
Because of the nature of the language, you have no magic bullet to use to kill side effects in C++. However, through disciplined coding, you can remove the side effects by observing some basic rules:
switch
statement rather than if…then
statements.The NoSideEffects
example, shown in Listing 1-5, demonstrates these principles. No matter what you do outside the function, nothing changes the result given a particular input.
LISTING 1-5: Producing Code without Side Effects
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int AddIt(const vector<int> Input,
const int Start, const int End) {
int Accumulate = 0;
// Copy the full vector to a vector of the
// correct size.
vector<int> Process(End - Start);
copy(&Input[Start], &Input[End], Process.begin());
// Create a sum using a foreach loop.
for (int Element : Process)
Accumulate += Element;
return Accumulate;
}
int main() {
const vector<int> ThisVector = {12, 2, 4, 18, 7, 2};
cout << "Sum of All Elements: " <<
AddIt(ThisVector, 0, ThisVector.size()) << endl;
cout << "Sum of Elements 1 through 4: " <<
AddIt(ThisVector, 1, 5) << endl;
return 0;
}
Everything in this example is handled as a constant except the Accumulate
and Process
variables inside the AddIt()
function. Consequently, there are no side effects. Any changes occur only within AddIt()
, and AddIt()
will always produce the same output for a given input.
To process just the ThisVector
elements that are needed for the summation, AddIt()
creates a copy of the Input
vector using the copy()
function. (Don’t worry about the use of a vector
right now; you see them explained in detail in Book 5, Chapter 6.) Notice that by using Standard Library functionality, you can avoid the appearance of state for the most part in this function. Even the foreach loop (implemented as a special case of the for
loop):
for (int Element : Process)
Accumulate += Element;
avoids the usual state information needed to power the loop. Theoretically, you could create a recursive solution to this problem that wouldn’t use state at all. Here’s the output from the example:
Sum of All Elements: 45
Sum of Elements 1 through 4: 31
Even though Listing 1-5 goes a long way toward making the C++ code easy to understand and considerably more bulletproof than you might otherwise expect, you can go one step further in that effort without resorting to anything odd in the way of coding. The Declarative
example shown in Listing 1-6 relies on the Standard Library even further to eliminate the need for a separate function.
LISTING 1-6: Using Declarative Programming Techniques
#include <iostream>
#include <array>
#include <numeric>
using namespace std;
int main() {
array<int, 6> ThisArray = {12, 2, 4, 18, 7, 2};
cout << "Sum of All Elements: " <<
accumulate(ThisArray.begin(), ThisArray.end(), 0)
<< endl;
cout << "Sum of Elements 1 through 4: " <<
accumulate(&ThisArray[1], &ThisArray[5], 0) << endl;
return 0;
}
This example uses the std::accumulate()
function to perform the required work. There are a number of interesting functions of this sort in the numeric
header, which you can see at https://en.cppreference.com/w/cpp/header/numeric
. Notice that the majority of these functions require C++ 11, C++ 17, or even C++ 20 to use, so they’re more appropriate for new development. The output from this example is precisely the same as the output from Listing 1-5; only the technique changes.
Notice also the two methods used to provide the starting and ending points for the calculation. What you need is an address. The first call uses the begin()
and end()
functions to supply the address, and the second call relies on the address provided by the []
operator.
Starting with C++ 11, you can use the auto
keyword in place of a specific type declaration. The use of the auto
keyword comes in handy when you don’t know what data type to expect in advance. When you run the application, the runtime deduces the type of the variable so that you can work with it correctly. Using this technique helps you create flexible code, even if it does reduce the clarity of your code a little. The Auto
example, shown in Listing 1-7, shows how to use this keyword to perform various tasks.
LISTING 1-7: Using the auto Keyword
#include <iostream>
#include <typeinfo>
using namespace std;
void DisplayIt(auto Value) {
cout << Value << " is of the " <<
typeid(Value).name() << " type." << endl;
}
int main() {
auto Hello1 = "Hello There!";
string Hello2 = "Hello Again!";
auto Number1 = 1234;
int Number2 = 5678;
auto Float1 = 12.34;
float Float2 = 56.78;
auto Boolean1 = true;
bool Boolean2 = false;
DisplayIt(Hello1);
DisplayIt(Hello2);
DisplayIt(Number1);
DisplayIt(Number2);
DisplayIt(Float1);
DisplayIt(Float2);
DisplayIt(Boolean1);
DisplayIt(Boolean2);
return 0;
}
The code begins by creating a number of variables — with half using standard declarations and half using the auto
keyword. It then calls DisplayIt()
to display the variable value and type. By using the auto keyword, DisplayIt()
can accept all these inputs and interact with them appropriately.
Hello There! is of the PKc type.
Hello Again! is of the NSt7__cxx1112basic_stringIcSt11char
_traitsIcESaIcEEE type.
1234 is of the i type.
5678 is of the i type.
12.34 is of the d type.
56.78 is of the f type.
1 is of the b type.
0 is of the b type.
Although you can probably figure the i
, d
, f
, and b
entries out, the PKc
entry is a mystery, and forget trying to determine the type of the next line that begins with NSt7
. You’ll likely want the output in human-readable form, which requires a few additional steps, starting with the addition of two new #include
entries.
#include <memory>
#include <cxxabi.h>
The DemangleIt()
function takes the mangled input from DisplayIt()
and forms it into a human-readable string, as shown here:
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";
}
The call to abi::__cxa_demangle()
performs the actual result. What you receive is a unique_ptr
, Result
, that contains a pointer to the human-readable form of the type. If the abi::__cxa_demangle()
call isn’t successful, Result
will contain a null pointer, and you can return a result of "Error"
in place of the actual type string. To make this code functional, you need to modify DisplayIt()
, as shown here:
void DisplayIt(auto Value) {
cout << Value << " is of the " <<
DemangleIt(typeid(Value).name()) << " type." << endl;
}
Now when you run the example, you see the output in human-readable form, which makes working with it a lot easier.
Hello There! is of the char const* type.
Hello Again! is of the std::__cxx11::basic_string<char,
std::char_traits<char>, std::allocator<char> > type.
1234 is of the int type.
5678 is of the int type.
12.34 is of the double type.
56.78 is of the float type.
1 is of the bool type.
0 is of the bool type.
Sometimes you need to apply a process to a group of numbers, or you need to apply more than one process to a single number. In fact, sometimes you need to do both. When you encounter situations like this, the easiest method of dealing with them is to pass a function, the process you want to perform, to another function that handles the situation. In the sections that follow, you begin by seeing a simple example of performing this task on a single number using multiple processes. You also see how to apply a single process to a group of numbers in a technique called a transform, because you’re transforming one series of numbers into another series of numbers.
At times, a single number represents a base value, but you must manipulate it in various ways to achieve a result. For example, you might need to find the correct process to use to optimize a particular set of values using a base value as a starting point. The FunctionFunction
example, shown in Listing 1-8, demonstrates how to use this technique.
LISTING 1-8: Passing a Function to a Function
#include <iostream>
#include <vector>
using namespace std;
int AddSome(int Value) {
return Value + 10;
}
int DelSome(int Value) {
return Value - 10;
}
int MulSome(int Value) {
return Value * 10;
}
int DivSome(int Value) {
return Value / 10;
}
typedef int(*FuncPtr)(int);
void ModIt(int Value, vector<FuncPtr> FuncArray) {
int NumFunc = FuncArray.size();
cout << "Processing " << NumFunc << " functions."
<< endl;
for(int i = 0; i < NumFunc; i++)
cout << FuncArray[i](Value) << endl;
}
int main() {
vector<FuncPtr> FuncArray =
{*AddSome, *DelSome, *MulSome, *DivSome};
ModIt(10, FuncArray);
return 0;
}
In most cases when you use this technique, you create an array or vector of function pointers. Using a vector is more flexible because you don’t have to predetermine the number of functions to pass — it can be any number up to the maximum size of the vector. To make this technique work, however, you must begin by creating a typedef
that defines the form of each function pointer entry consisting of the
int
(*FuncPtr)
(int)
You define the vector as vector<FuncPtr>
with a vector name, such as FuncArray
. Creating the vector then becomes a matter of providing pointers to the four functions used for testing in this case: AddSome()
, DelSome()
, MulSome()
, and DivSome()
. These four functions don’t do much, but they do help in testing.
The code calls ModIt()
with the value you want to work with, which is 10
, and the vector of function pointers, FuncArray
. Inside ModIt()
, the code calls each of the functions in turn with the supplied value and outputs the result onscreen. Here is the output from this example:
Processing 4 functions.
20
0
100
1
A transform allows you to process a series of values using a single function. Combining a series of transforms enables you to process a series of values using a series of functions in a particular order. You see transforms used in all sorts of ways, including to condition data and process video. The Transform
example, shown in Listing 1-9, gives you an overview of how this technique works using the C++ range functionality.
LISTING 1-9: Using a Transform on a Series of Data Points
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct EvenPair {
int Value;
bool Even;
};
EvenPair IsEven(int Value){
if (Value % 2 == 0)
return EvenPair{Value, true};
return EvenPair{Value, false};
}
int main(){
vector<int> Values{1, 2, 3, 4};
vector<EvenPair> Evens(Values.size());
transform(Values.begin(), Values.end(),
Evens.begin(), IsEven);
for(auto isEven : Evens)
if (isEven.Even)
cout << isEven.Value << " is even." << endl;
else
cout << isEven.Value << " is odd." << endl;
return 0;
}
This example uses the EvenPair
structure to hold two variables that contain the original value you want to check and show whether that value is even. In main()
, you begin by creating two vectors: one input, Values
, and one output, Evens
. The Evens
vector will contain a list of the original values and a Boolean showing whether each value is even.
After the transformation completes, a foreach loop checks each value in Evens
and outputs an appropriate string. Here are the results:
1 is odd.
2 is even.
3 is odd.
4 is even.
A lambda expression is an unnamed function that you can use in place of a regular function reference. Using a lambda expression can make your code more readable by placing the function inline. Chapters 2 and 3 of this minibook cover lambda expressions in detail, but the Lambda
example, shown in Listing 1-10, shows an alternative way to create the code displayed in Listing 1-9 in a shorter way.
LISTING 1-10: Performing a Transform Using a Lambda Expression
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct EvenPair {
int Value;
bool Even;
};
int main(){
vector<int> Values{1, 2, 3, 4};
vector<EvenPair> Evens(Values.size());
transform(Values.begin(), Values.end(),
Evens.begin(), [](int Value) {
return (Value % 2 == 0)
? EvenPair{Value, true}
: EvenPair{Value, false};});
for(auto isEven : Evens)
if (isEven.Even)
cout << isEven.Value << " is even." << endl;
else
cout << isEven.Value << " is odd." << endl;
return 0;
}
The basic idea of this example is the same as the example in the “Using transforms” section, earlier in this chapter, except that it uses a lambda expression in place of the call to IsEven()
. The lambda expression begins with a capture clause, []
, which defines how to capture any required external variables. An empty capture clause says that the lambda expression can work only with variables that are local to it, which is Value
in this case.
As with IsEven()
, the lambda expression requires an int
input, Value
. The compiler deduces the output type based on the lambda expression code. However, you can specify the output type directly when needed using ->
output_type
. In this case, you’d use [](int Value) -> EvenPair
in place of the code shown.
The output is one of two values, as determined by a ternary operator. When (Value % 2 == 0)
is true, the output is EvenPair{Value, true}
; otherwise, the output is EvenPair{Value, false}
. The point is that this version is shorter than the version in Listing 1-9, so lambda expressions can make your code shorter and easier to understand when the function you want to use is small.