自 90 年代以来,标准模板库 ( STL )一直是 C++ 程序员的有用伴侣。从泛型编程和值语义这样的概念开始,它已经成长为支持许多有用的场景。在本章中,我们将了解 STL 如何支持 C++ 17 中的函数式编程,并了解 C++ 20 中引入的一些新特性。
本章将涵盖以下主题:
- 使用
<functional>
标题中的功能特性 - 使用
<numeric>
标题中的功能特性 - 使用
<algorithm>
标题中的功能特性 std::optional
和std::variant
- C++ 20 和范围库
您将需要一个支持 C++ 17 的编译器。我用的是 GCC 7.4.0c.
代码在的 GitHub 上。com/ PacktPublishing/动手-函数-用- Cpp 编程Chapter15
文件夹中的。它包含并使用了doctest
,这是一个单头开源单元测试库。你可以在它的 GitHub 存储库中找到它:这里: https:/ /github。com/ onqtam/ doctest 。
我们需要从某个地方开始探索 STL 中的函数式编程支持,标题名为<functional>
似乎是一个好的开始。该标题定义了基本的function<>
类型,我们可以将其用于函数,并且在本书中已经多次用于 lambdas:
TEST_CASE("Identity function"){
function<int(int)> identity = [](int value) { return value;};
CHECK_EQ(1, identity(1));
}
我们可以使用function<>
类型来存储任何类型的函数,无论是自由函数、成员函数还是 lambda。让我们看一个自由函数的例子:
TEST_CASE("Free function"){
function<int()> f = freeFunctionReturns2;
CHECK_EQ(2, f());
}
下面是一个成员函数的例子:
class JustAClass{
public:
int functionReturns2() const { return 2; };
};
TEST_CASE("Class method"){
function<int(const JustAClass&)> f = &JustAClass::functionReturns2;
JustAClass justAClass;
CHECK_EQ(2, f(justAClass));
}
如您所见,为了通过function<>
类型调用成员函数,需要传入对对象的有效引用。把它想象成*this
的例子。
除了这种基本类型之外,<functional>
头提供了一些已经定义的函数对象,当在集合上使用函数转换时,这些对象会派上用场。让我们看一个简单的例子,结合使用sort
算法和定义的greater
函数,以降序对向量进行排序:
TEST_CASE("Sort with predefined function"){
vector<int> values{3, 1, 2, 20, 7, 5, 14};
vector<int> expectedDescendingOrder{20, 14, 7, 5, 3, 2, 1};
sort(values.begin(), values.end(), greater<int>());
CHECK_EQ(expectedDescendingOrder, values);
}
<functional>
标题定义了以下有用的功能对象:
- 算术运算 :
plus
、minus
、multiplies
、divides
、modulus
和negate
- 对比 :
equal_to
、not_equal_to
、greater
、less
、greater_equal
和less_equal
- 逻辑运算 :
logical_and
、logical_or
和logical_not
- 逐位操作 :
bit_and
、bit_or
和bit_xor
当我们需要使用高阶函数时,这些函数对象免去了我们将常见操作封装在函数中的麻烦。虽然这是一个很棒的集合,但我敢说身份函数也同样有用,尽管听起来很奇怪。幸运的是,实现一个很容易。
然而,这并不是<functional>
标题所能提供的全部。bind
功能实现部分功能应用。我们在本书中已经多次看到它在行动中的运用,在第五章、局部运用和 Currying 中可以详细看到它的用法。它的基本功能是取一个函数,将一个或多个参数绑定到值,并获得一个新的函数:
TEST_CASE("Partial application using bind"){
auto add = [](int first, int second){
return first + second;
};
auto increment = bind(add, _1, 1);
CHECK_EQ(3, add(1, 2));
CHECK_EQ(3, increment(2));
}
由于function<>
类型允许我们编写 lambdas,预定义的函数对象减少了重复,而bind
允许部分应用,我们有了以函数方式构造代码的基础。但是如果没有高阶函数,我们就无法做到这一点。
<algorithm>
头文件包含算法,其中一些算法实现为高阶函数。在这本书里,我们已经看到了许多使用它们的例子。以下是一些有用的算法:
all_of
、any_of
和none_of
find_if
和find_if_not
count_if
copy_if
generate_n
sort
我们已经看到,关注数据并结合这些高阶函数,将输入数据转换为所需的输出,是您在小型、可组合的纯函数中思考的方式之一。我们还看到了这种方法的缺点——需要复制数据,或者对同一数据进行多次传递——我们还看到了新的范围库如何以优雅的方式解决这些问题。
虽然所有这些函数都非常有用,但是<algorithm>
命名空间中有一个函数值得特别一提——函数map
操作的实现,transform
。transform
函数获取一个输入集合,并对集合中的每个元素应用一个λ,返回一个新的集合,该集合具有相同数量的元素,但其中存储了转换后的值。这为根据我们的需求调整数据结构打开了无限的可能性。我们来看几个例子。
我们经常需要从集合中的每个元素获取属性值。在下面的例子中,我们使用transform
从一个向量中获得所有人名的列表:
TEST_CASE("Project names from a vector of people"){
vector<Person> people = {
Person("Alex", 42),
Person("John", 21),
Person("Jane", 14)
};
vector<string> expectedNames{"Alex", "John", "Jane"};
vector<string> names = transformAll<vector<string>>(
people,
[](Person person) { return person.name; }
);
CHECK_EQ(expectedNames, names);
}
我们再次在transform
和transformAll
上使用包装器,以避免编写样板代码:
template<typename DestinationType>
auto transformAll = [](auto source, auto lambda){
DestinationType result;
transform(source.begin(), source.end(), back_inserter(result),
lambda);
return result;
};
有时,我们需要计算一个条件是否适用于一组元素。在下面的例子中,我们将通过比较年龄和18
:
来计算人们是否是未成年人
TEST_CASE("Minor or major"){
vector<Person> people = {
Person("Alex", 42),
Person("John", 21),
Person("Jane", 14)
};
vector<bool> expectedIsMinor{false, false, true};
vector<bool> isMinor = transformAll<vector<bool>>(
people,
[](Person person) { return person.age < 18; }
);
CHECK_EQ(expectedIsMinor, isMinor);
}
我们经常需要保存或显示一个列表。为此,我们需要将列表的每个元素转换为可显示或可序列化的格式。在下面的例子中,我们正在计算列表中Person
对象的 JSON 表示:
TEST_CASE("String representation"){
vector<Person> people = {
Person("Alex", 42),
Person("John", 21),
Person("Jane", 14)
};
vector<string> expectedJSON{
"{'person': {'name': 'Alex', 'age': '42'}}",
"{'person': {'name': 'John', 'age': '21'}}",
"{'person': {'name': 'Jane', 'age': '14'}}"
};
vector<string> peopleAsJson = transformAll<vector<string>>(
people,
[](Person person) {
return
"{'person': {'name': '" + person.name + "', 'age':
'" + to_string(person.age) + "'}}"; }
);
CHECK_EQ(expectedJSON, peopleAsJson);
}
即使transform
函数提供了无限的可能性,它在与reduce
(【c++ 中的 T2】)高阶函数的结合中变得更加强大。
有趣的是,形成函数式编程中最常见的模式之一map
/ reduce
模式的两个高阶函数最终出现在 C++ 中的两个不同头文件中。transform
/ accumulate
组合需要<algorithm>
和<numeric>
头文件,允许我们解决具有以下模式的许多问题:
- 提供了一个集合。
- 该系列需要转化为其他产品。
- 需要计算聚合结果。
我们来看几个例子。
假设我们有一个Product
结构,如下所示:
struct Product{
string name;
string category;
double price;
Product(string name, string category, double price): name(name),
category(category), price(price){}
};
我们还假设我们根据产品类别有不同的税收水平:
map<string, int> taxLevelByCategory = {
{"book", 5},
{"cosmetics", 20},
{"food", 10},
{"alcohol", 40}
};
假设我们得到了一个产品列表,如下所示:
vector<Product> products = {
Product("Lord of the Rings", "book", 22.50),
Product("Nivea", "cosmetics", 15.40),
Product("apple", "food", 0.30),
Product("Lagavulin", "alcohol", 75.35)
};
让我们计算一下含税和不含税的总价。我们还有一个助手包装器accumulateAll
,供我们使用:
auto accumulateAll = [](auto collection, auto initialValue, auto
lambda){
return accumulate(collection.begin(), collection.end(),
initialValue, lambda);
};
要计算不含税的价格,我们只需要把所有的产品价格加起来。这是典型的map
/ reduce
场景:
auto totalWithoutTax = accumulateAll(transformAll<vector<double>>
(products, [](Product product) { return product.price; }), 0.0,
plus<double>());
CHECK_EQ(113.55, doctest::Approx(totalWithoutTax));
首先,我们将Products
的列表map
( transform
)转换成价格列表,然后将reduce
(或accumulate
)转换成单个值——它的总值。
当我们需要含税的总价格时,类似的,尽管更复杂的过程也适用:
auto pricesWithTax = transformAll<vector<double>>(products,
[](Product product){
int taxPercentage =
taxLevelByCategory[product.category];
return product.price + product.price *
taxPercentage/100;
});
auto totalWithTax = accumulateAll(pricesWithTax, 0.0,
plus<double> ());
CHECK_EQ(147.925, doctest::Approx(totalWithTax));
首先我们map
( transform
)把Products
的清单跟含税的价格清单联系起来,然后reduce
(或者accumulate
)把所有的数值都跟含税的总额联系起来。
如果你想知道的话,doctest::Approx
函数允许在有小舍入误差的浮点数之间进行比较。
在前一节中,我们看到了如何通过transform
调用将列表中的每一项转换为 JSON。借助accumulate
很容易将其转化为完整的 JSON 列表:
string expectedJSONList = "{people: {'person': {'name': 'Alex',
'age': '42'}}, {'person': {'name': 'John', 'age': '21'}},
{'person': {'name': 'Jane', 'age': '14'}}}";
string peopleAsJSONList = "{people: " + accumulateAll(peopleAsJson,
string(),
[](string first, string second){
return (first.empty()) ? second : (first + ", " +
second);
}) + "}";
CHECK_EQ(expectedJSONList, peopleAsJSONList);
我们使用transform
将人员列表变成每个对象的 JSON 表示的列表,然后使用accumulate
将它们连接起来,并使用一些额外的操作在 JSON 中添加列表表示的前面和后面。
如你所见,transform
/ accumulate
(或map
/ reduce
)组合有很多不同的用途,这取决于我们传递给它的功能。
我们可以用transform
、accumulate
、any_of
/ all_of
/ none_of
完成很多事情。然而,有时我们需要从集合中过滤掉一些数据。
通常的做法是find_if
。然而,如果我们需要从一个集合中找到符合特定条件的所有项目,那么find_if
就很麻烦。因此,使用 C++ 17 标准以函数方式解决这个问题的最佳选择是copy_if
。以下示例使用copy_if
查找人员列表中的所有未成年人:
TEST_CASE("Find all minors"){
vector<Person> people = {
Person("Alex", 42),
Person("John", 21),
Person("Jane", 14),
Person("Diana", 9)
};
vector<Person> expectedMinors{Person("Jane", 14),
Person("Diana", 9)};
vector<Person> minors;
copy_if(people.begin(), people.end(), back_inserter(minors), []
(Person& person){ return person.age < 18; });
CHECK_EQ(minors, expectedMinors);
}
我们已经讨论了很多 happy path 案例,即数据对我们的数据转换有效的时候。我们如何处理边缘情况和错误?当然,在例外情况下,我们可以抛出异常或返回错误情况,但是当我们需要返回错误消息时,情况会怎样呢?
在这些情况下,函数方式是返回数据结构。毕竟,即使输入无效,我们也需要返回一个输出值。但是我们遇到了一个挑战——在错误的情况下,我们需要返回的类型是错误类型,而在有效数据的情况下,我们需要返回的类型是一些更有效的数据。
幸运的是,在这些情况下,我们有两种结构支持我们——std::optional
和std::variant
。让我们举一个人员列表的例子,其中一些是有效的,另一些是无效的:
vector<Person> people = {
Person("Alex", 42),
Person("John", 21),
Person("Jane", 14),
Person("Diana", 0)
};
最后一个人的年龄无效。让我们试着用函数的方式编写代码,显示以下字符串:
Alex, major
John, major
Jane, minor
Invalid person
为了有一个转换链,我们需要使用optional
类型,如下所示:
struct MajorOrMinorPerson{
Person person;
optional<string> majorOrMinor;
MajorOrMinorPerson(Person person, string majorOrMinor) :
person(person), majorOrMinor(optional<string>(majorOrMinor)){};
MajorOrMinorPerson(Person person) : person(person),
majorOrMinor(nullopt){};
};
auto majorMinorPersons = transformAll<vector<MajorOrMinorPerson>>
(people, [](Person& person){
if(person.age <= 0) return MajorOrMinorPerson(person);
if(person.age > 0 && person.age < 18) return
MajorOrMinorPerson(person, "minor");
return MajorOrMinorPerson(person, "major");
});
通过这个调用,我们获得了这个人和一个值之间的配对列表,该值可以是nullopt
、minor
或major
。我们可以在下面的transform
调用中使用它,以便根据有效性条件获取字符串列表:
auto majorMinorPersonsAsString = transformAll<vector<string>>
(majorMinorPersons, [](MajorOrMinorPerson majorOrMinorPerson){
return majorOrMinorPerson.majorOrMinor ?
majorOrMinorPerson.person.name + ", " +
majorOrMinorPerson.majorOrMinor.value() :
"Invalid person";
});
最后,对累加的调用创建了预期的输出字符串:
auto completeString = accumulateAll(majorMinorPersonsAsString,
string(), [](string first, string second){
return first.empty() ? second : (first + "\n" + second);
});
我们可以通过一个测试来检验这一点:
string expectedString("Alex, major\nJohn, major\nJane,
minor\nInvalid person");
CHECK_EQ(expectedString, completeString);
另一种方法是使用variant
,如果我们需要,例如,返回一个错误代码结合人。
我们在第 14 章、使用范围库的延迟求值中详细讨论了范围库。如果您可以使用它,或者因为您使用 C++ 20,或者因为您可以将它用作第三方库,那么前面的函数将变得极其简单,而且速度更快:
TEST_CASE("Ranges"){
vector<Person> people = {
Person("Alex", 42),
Person("John", 21),
Person("Jane", 14),
Person("Diana", 0)
};
using namespace ranges;
string completeString = ranges::accumulate(
people |
view::transform(personToMajorMinor) |
view::transform(majorMinor),
string(),
combineWithNewline
);
string expectedString("Alex, major\nJohn, major\nJane,
minor\nInvalid person");
CHECK_EQ(expectedString, completeString);
}
同样,从人员列表中查找未成年人列表非常容易,范围为'view::filter
:
TEST_CASE("Find all minors with ranges"){
using namespace ranges;
vector<Person> people = {
Person("Alex", 42),
Person("John", 21),
Person("Jane", 14),
Person("Diana", 9)
};
vector<Person> expectedMinors{Person("Jane", 14),
Person("Diana", 9)};
vector<Person> minors = people | view::filter(isMinor);
CHECK_EQ(minors, expectedMinors);
}
一旦我们有了isMinor
谓词,我们就可以将其传递给view::filter
来从人员列表中找到未成年人。
在这一章中,我们介绍了 C++ 17 的 STL 中可用的函数式编程特性,以及 C++ 20 中的新特性。有了函数、算法、variant
和optional
在错误或边缘情况下提供的帮助,以及使用范围库可以实现的简化和优化的代码,我们对函数编程特性有了很好的支持。
现在,是时候进入下一章,看看 C++ 17 语言对函数式编程的支持,以及 C++ 20 中函数式编程的有趣之处。