Skip to content

Latest commit

 

History

History
470 lines (351 loc) · 17.8 KB

File metadata and controls

470 lines (351 loc) · 17.8 KB

十五、STL 支持和建议

自 90 年代以来,标准模板库 ( STL )一直是 C++ 程序员的有用伴侣。从泛型编程和值语义这样的概念开始,它已经成长为支持许多有用的场景。在本章中,我们将了解 STL 如何支持 C++ 17 中的函数式编程,并了解 C++ 20 中引入的一些新特性。

本章将涵盖以下主题:

  • 使用<functional>标题中的功能特性
  • 使用<numeric>标题中的功能特性
  • 使用<algorithm>标题中的功能特性
  • std::optionalstd::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>标题定义了以下有用的功能对象:

  • 算术运算 : plusminusmultipliesdividesmodulusnegate
  • 对比 : equal_tonot_equal_togreaterlessgreater_equalless_equal
  • 逻辑运算 : logical_andlogical_orlogical_not
  • 逐位操作 : bit_andbit_orbit_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_ofany_ofnone_of
  • find_iffind_if_not
  • count_if
  • copy_if
  • generate_n
  • sort

我们已经看到,关注数据并结合这些高阶函数,将输入数据转换为所需的输出,是您在小型、可组合的纯函数中思考的方式之一。我们还看到了这种方法的缺点——需要复制数据,或者对同一数据进行多次传递——我们还看到了新的范围库如何以优雅的方式解决这些问题。

虽然所有这些函数都非常有用,但是<algorithm>命名空间中有一个函数值得特别一提——函数map操作的实现,transformtransform函数获取一个输入集合,并对集合中的每个元素应用一个λ,返回一个新的集合,该集合具有相同数量的元素,但其中存储了转换后的值。这为根据我们的需求调整数据结构打开了无限的可能性。我们来看几个例子。

从集合中投影每个对象的一个属性

我们经常需要从集合中的每个元素获取属性值。在下面的例子中,我们使用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);
}

我们再次在transformtransformAll上使用包装器,以避免编写样板代码:

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函数允许在有小舍入误差的浮点数之间进行比较。

将列表转换为 JSON

在前一节中,我们看到了如何通过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)组合有很多不同的用途,这取决于我们传递给它的功能。

返回–查找 _if 并复制 _if

我们可以用transformaccumulateany_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::optionalstd::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");
            });

通过这个调用,我们获得了这个人和一个值之间的配对列表,该值可以是nulloptminormajor。我们可以在下面的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,如果我们需要,例如,返回一个错误代码结合人。

C++ 20 和范围库

我们在第 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 中的新特性。有了函数、算法、variantoptional在错误或边缘情况下提供的帮助,以及使用范围库可以实现的简化和优化的代码,我们对函数编程特性有了很好的支持。

现在,是时候进入下一章,看看 C++ 17 语言对函数式编程的支持,以及 C++ 20 中函数式编程的有趣之处。