Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
roth_stephan_clean_c20_sustainable_software_development_patt.pdf
Скачиваний:
29
Добавлен:
27.03.2023
Размер:
7.26 Mб
Скачать

Chapter 7 Functional Programming

Note  Until C++11, it was a good practice that functors, depending on their number of parameters, were derived from the templates std::unary_function and std::binary_function (both defined in the <functional> header).

These templates have been labeled as deprecated with C++11 and have been removed from the Standard Library since C++17.

Binders and Function Wrappers

The next development step in terms of functional programming in C++ was made with the publication of the draft C++ Technical Report 1 (TR 1) in 2005, which is the common name for the standard ISO/IEC TR 19768:2007 C++ Library Extensions. The TR 1 specifies a series of extensions to the C++ Standard Library, including, among other things, extensions for functional programming. This technical report was the library extension proposal for the later C++11 standard, and in fact, 12 of the 13 proposed libraries (with slight modifications) also made it into the new language standard published in 2011.

In terms of functional programming, the TR 1 introduced the two function templates std::bind and std::function, which are defined in the <functional> library header.

The function template std::bind is a binder wrapper for functions and their arguments. You can take a function (or a function pointer, or a functor), and “bind” actual values to one or all of the function’s parameters. In other words, you can create new function-like objects from existing functions or functors. Let’s start with a simple example, as shown in Listing 7-17.

Listing 7-17.  Using std::bind to Wrap the multiply() Binary Function

#include <functional> #include <iostream>

[[nodiscard]] constexpr double multiply(const double multiplicand, const double multiplier) noexcept {

return multiplicand * multiplier;

}

int main() {

const auto result1 = multiply(10.0, 5.0);

312

Chapter 7 Functional Programming

auto boundMultiplyFunctor = std::bind(multiply, 10.0, 5.0); const auto result2 = boundMultiplyFunctor();

std::cout << "result1 = " << result1 << ", result2 = " << result2 << std::endl;

return 0;

}

In this example, the multiply() function is wrapped, together with two floating-­ point number literals (10.0 and 5.0), using std::bind. The number literals represent the actual parameters that are bound to the two function arguments multiplicand and multiplier. As a result, we get a new function-like object that is stored in the boundMultiplyFunctor variable. It can then be called like an ordinary functor using the parenthesis operator.

Maybe you are wondering, nice, but I don’t get it. What’s the purpose of that? What is the practical benefit of the binder function template?

Well, std::bind allows something that is known as partial application (or partial function application) in programming. Partial application is a process by which only a subset of the function parameters is bound to values or variables, whereas the other part is not yet bound. The unbound parameters are replaced with the placeholders _1, _2, _3, and so on, which are defined in the namespace std::placeholders. See Listing 7-18.

Listing 7-18.  An Example of Partial Function Application

#include <functional> #include <iostream>

[[nodiscard]] constexpr double multiply(const double multiplicand, const double multiplier) noexcept {

return multiplicand * multiplier;

}

int main() {

using namespace std::placeholders;

auto multiplyWith10 = std::bind(multiply, _1, 10.0); std::cout << "result = " << multiplyWith10(5.0) << std::endl; return 0;

}

313

Chapter 7 Functional Programming

In this example, the second parameter of the multiply() function is bound to the floating-point number literal 10.0, but the first parameter is bound to a placeholder. The function-like object, which is the return value of std::bind(), is stored in the multiplyWith10 variable. This variable can now be used like a function, but we only need to pass one parameter: the value that is to be multiplied by 10.0.

Partial function application is an adaptation technique that allows us to use a function or a functor in various situations, when we need their functionality, but when we can supply some but not all of the arguments. In addition, with the help of the placeholders, the order of the functions parameters can be adapted to the order that the client code expects. For example, the position of the multiplicand and the multiplier in the parameter list can be interchanged by mapping them to a new function-like object in the following way:

auto multiplyWithExchangedParameterPosition = std::bind(multiply, _2, _1);

In our case with the multiply() function, this is obviously senseless (remember the commutative property of multiplication), because the new function object will produce the same results as the original multiply() function. However, in other situations, adapting the order of the parameters can improve the usability of a function. Partial function application is a tool for interface adaptation.

By the way, especially in conjunction with functions as return parameters, the automatic type deduction with its keyword auto (see the section entitled “Automatic Type Deduction” in Chapter 5) can provide valuable services, because if we inspect what the GCC compiler returns from the call to std::bind(), it is an object of the following complex type:

std::_Bind_helper<bool0,double (&)(double, double),const _Placeholder<int2> &,const _Placeholder<int1> &>::type

Terrifying, isn’t it? Writing down such a type explicitly in source code is not only less helpful, but apart from that the readability of the code also suffers considerably. Thanks to the keyword auto, it is not necessary to define these types explicitly. But in those rare cases, where you must do it, the class template std::function comes into play, which is a general-purpose polymorphic function wrapper. This template can wrap an arbitrary callable object (an ordinary function, a functor, a function pointer, etc.) and manages the memory used to store that object. For example, to wrap our multiplication function multiply() into a std::function object, the code looks as follows:

314

Chapter 7 Functional Programming

std::function<double(double, double)> multiplyFunc = multiply; auto result = multiplyFunc(10.0, 5.0);

Now that we’ve discussed std::bind, std::function, and the technique of partial application, I have a possibly disappointing message for you: since C++11 and the introduction of lambda expressions, most of this template stuff from the C++ Standard Library is only seldom required.

Lambda Expressions

With the advent of C++11, the language has been extended with a new and noteworthy feature: lambda expressions! Other frequently used terms for them are lambda functions, function literals, or just lambdas. Sometimes they are also called closures, which is actually a general term from functional programming, and which, incidentally, is also not entirely correct.

CLOSURE

In imperative programming languages, we are accustomed to the fact that a variable is no longer available when the program execution leaves the scope within which the variable is defined. For instance, if a function is done and returns to its caller, all local variables of that function are removed from the call stack and deleted from memory.

On the other hand, in functional programming, we can build a closure, which is a function object with a persistent local variable scope. In other words, closures allow a scope with some or all of its local variables to be tied to a function, and this scope object will persist as long as that function exists.

In C++, such closures can be created with the help of lambda expressions due to the capture list in the lambda introducer. A closure is not the same as a lambda expression, just like an object (instance) in object orientation is not the same as its class.

315

Chapter 7 Functional Programming

What is special about lambda expressions is that they are usually implemented inline, that is, at the point of their application. This can sometimes improve the readability of the code, and compilers can apply their optimization strategies even more efficiently. Of course, lambda functions can also be treated as data, for example, stored in variables or passed as a function argument to a so-called high-order function (see the next section about this topic).

The basic structure of a lambda expression looks as follows:

[ capture list ](parameter list) -> return_type_declaration { lambda body }

Since this book is not a C++ language introduction, I will not explain all the basics of lambda expressions here. Even if you are seeing something like this for the first time, it should be relatively clear that the return type, the parameter list, and the lambda body are pretty much the same as with ordinary functions. What might seem unusual at first glance are two things. For example, a lambda expression has no name like an ordinary function or a function-like object. This is the reason that one speaks in this context of anonymous functions. The other conspicuousness is the square bracket at the beginning, which is also called the lambda introducer. As the name suggests, the lambda introducer marks the beginning of a lambda expression. In addition, the introducer also optionally contains something called a capture list.

What makes this capture list so important is that all the variables from the outside scope are listed, which should be available inside of the lambda body, and whether they should be captured by value (copying) or by reference. In other words, these are the closures of the lambda expression.

An example lambda expression is defined as follows:

[](const double multiplicand, const double multiplier) { return multiplicand * multiplier; }

This is our good old multiplication function from the previous section as a lambda. The introducer has a blank capture list, which means that nothing from the surrounding scope is used. The return type is not specified in this case either, because the compiler can easily deduce it.

By assigning the lambda expression to a variable, a corresponding runtime object is created, the so-called closure. And this is actually true: the compiler generates a functor class of an unspecified type from a lambda expression, which is instantiated at runtime and assigned to the variable. The captures in the capture list are converted into

316

Chapter 7 Functional Programming

constructor parameters and member variables of the functor object. The parameters in the lambda’s parameter list are turned into parameters for the functor’s parenthesis operator (operator()). See Listing 7-19.

Listing 7-19.  Using the Lambda Expression to Multiply Two Doubles

#include <iostream>

int main() {

auto multiply = [](const double multiplicand, const double multiplier) { return multiplicand * multiplier;

};

std::cout << multiply(10.0, 50.0) << std::endl; return 0;

}

However, the whole thing can be shorter, because a lambda expression can be called directly at the place of its definition by appending parentheses with arguments behind the lambda body. See Listing 7-20.

Listing 7-20.  Defining and Calling a Lambda Expression in One Go

int main() { std::cout <<

[](const double multiplicand, const double multiplier) { return multiplicand * multiplier;

}(50.0, 10.0) << std::endl; return 0;

}

The previous example is, of course, for demonstration purposes only, since the use of a lambda in this style makes no sense. The example in Listing 7-21 uses two lambda expressions. One is used by the algorithm called std::transform to envelop the words in the string vector called quote with angle brackets and store them in another vector named result. The other lambda expression is used by std::for_each to output the content of result on standard output.

317