C++-函数式编程学习手册(全)
C++ 函数式编程学习手册(全)
原文:
annas-archive.org/md5/8ba9d5d0c71497e4f1c908aec7505b42
译者:飞龙
前言
函数式编程是一种通过组合纯函数构建计算机程序的元素和结构的风格,避免共享状态、可变数据和副作用,就像我们通常在数学中看到的那样。代码函数中的变量表示函数参数的值,并且类似于数学函数。这个想法是程序员定义包含表达式、定义和可以用变量表示的参数的函数来解决问题。
函数式编程是声明式的而不是命令式的,这意味着编程是通过表达式或声明而不是语句完成的。函数式编程的应用状态通过纯函数流动,因此它避免了副作用。与命令式编程相比,应用状态通常与对象中的方法共享和共存。在命令式编程中,表达式被评估,并且结果值被赋给变量。例如,当我们将一系列表达式组合成一个函数时,结果值取决于该时刻变量的状态。由于状态不断变化,评估的顺序很重要。在函数式编程中,禁止破坏性赋值,每次赋值发生时,都会引入一个新变量。最重要的是,函数式代码往往更简洁、可预测,比命令式或面向对象的代码更容易测试。
尽管有一些专门设计用于函数式编程的语言,比如 Haskell 和 Scala,我们也可以使用 C++来完成函数式编程的设计,正如我们将在本书中讨论的那样。
本书内容
《第一章》《现代 C++的深入探讨》概述了现代 C++,包括现代 C++中几个新特性的实现,比如 auto 关键字、decltype 关键字、空指针、基于范围的 for 循环、标准模板库、Lambda 表达式、智能指针和元组。
《第二章》《在函数式编程中操作函数》介绍了函数式编程中操作函数的基本技术;它们是第一类函数技术、纯函数和柯里化技术。通过应用第一类函数,我们可以将函数视为数据,这意味着它可以分配给任何变量,而不仅仅是作为函数调用。我们还将应用纯函数技术,使函数不再产生副作用。此外,为了简化函数,我们可以应用柯里化技术,通过在每个函数中评估一系列带有单个参数的函数来减少多参数函数。
《第三章》《将不可变状态应用于函数》解释了我们如何为可变对象实现不可变对象。我们还将深入研究第一类函数和纯函数,这些内容在上一章中讨论过,以产生一个不可变对象。
《第四章》《使用递归算法重复方法调用》讨论了迭代和递归的区别,以及为什么递归技术对函数式编程更好。我们还将列举三种递归:函数式、过程式和回溯递归。
《第五章》《使用惰性求值延迟执行过程》解释了如何延迟执行过程以获得更高效的代码。我们还将实现缓存和记忆化技术,使我们的代码运行更快。
第六章,使用元编程优化代码,讨论了使用元编程在编译时执行代码以优化代码。我们还将讨论如何将流程控制重构为模板元编程。
第七章,使用并发运行并行执行,向我们展示了如何在 C++编程中运行多个线程,以及如何同步线程以避免死锁。我们还将在 Windows 操作系统中应用线程处理。
第八章,使用函数式方法创建和调试应用程序,详细介绍了我们在前几章中讨论的所有技术,以设计函数式编程。此外,我们将尝试调试代码,以找到解决方案,如果出现意外结果或程序在执行中崩溃。
您需要什么来阅读本书
要阅读本书并成功编译所有源代码示例,您需要一台运行 Microsoft Windows 8.1(或更高版本)的个人电脑,并包含以下软件:
-
GCC 的最新版本,支持 C++11、C++14 和 C++17(在撰写本书时,最新版本是 GCC v7.1.0)
-
Microsoft Visual Studio 2017 提供的 Microsoft C++编译器,支持 C++11、C++14 和 C++17(适用于第七章,使用并发运行并行执行)
-
Code::Blocks v16.01(所有示例代码均使用 Code::Blocks IDE 编写;但是,使用此 IDE 是可选的)
这本书适合谁
本书适用于熟悉面向对象编程的 C++开发人员,他们有兴趣学习如何应用函数式范式来创建健壮且可测试的应用程序。
约定
在本书中,您将找到一些文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL 和用户输入显示如下:"auto
关键字也可以应用于函数,以自动推断函数的返回类型。"
代码块设置如下:
int add(int i, int j)
{
return i + j;
}
当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:
// Initializing a string variable
Name n = {"Frankie Kaur"};
cout << "Initial name = " << n.str;
cout << endl;
新术语和重要单词以粗体显示。
警告或重要提示会出现在这样的形式。
提示和技巧会出现在这样的形式。
第一章:深入现代 C++
自 1979 年发明以来,C++编程语言发生了巨大变化。在这个时代,有些人可能会有点害怕使用 C++语言编码,因为它不够用户友好。我们有时必须处理的内存管理有时会让人不愿意使用这种语言。幸运的是,自C++11--也被称为现代 C++,以及C++14和C++17--发布以来,已经引入了许多功能来简化我们在 C++语言中的代码。而且,最好的部分是 C++编程语言是任何项目的绝佳语言,从低级编程到 Web 编程,以及函数式编程。
这一章是我们在本书中开始旅程的最佳地方,因为它是为 C++程序员设计的,可以更新他们的知识,并将讨论以下主题:
-
理解现代 C++中的一些新功能
-
在现代 C++中实现 C++标准库
-
使用 Lambda 表达式和 C++ Lambda 中包含的所有功能
-
使用智能指针避免手动内存管理
-
使用元组处理多个返回值
深入了解现代 C++中的一些新功能
那么,现代 C++与旧版本相比有什么新功能?与旧版本相比,现代 C++中有很多变化,如果我们讨论所有这些变化,书页将大幅增加。然而,我们将讨论现代 C++中的新功能,我们应该了解这些功能,以使我们在编码活动中更加高效。我们将讨论几个新关键字,如auto
、decltype
和nullptr
。我们还将讨论begin()
和end()
函数的增强,这些函数现在已成为非成员类函数。我们还将讨论对使用range-based for loop
技术迭代集合的for-each
技术的增强支持。
本章的接下来几个小节还将讨论现代 C++的新功能,即 Lambda 表达式、智能指针和元组,这些功能刚刚在 C++11 发布中添加。
使用auto
关键字自动定义数据类型
在现代 C++之前,C++语言有一个名为auto
的关键字,用于明确指定变量应具有自动持续时间。遵循变量的自动持续时间将在定义点创建变量(如果相关,则初始化),并在退出定义它们的块时销毁变量。例如,局部变量将在函数开始时定义并在程序退出包含局部变量的函数时销毁。
自 C++11 以来,auto
关键字用于告诉编译器从其初始化程序推断出正在声明的变量的实际类型。自 C++14 以来,该关键字还可以应用于函数,以指定函数的返回类型,即尾随返回类型。现在,在现代 C++中,使用auto
关键字指定自动持续时间已被废除,因为默认情况下所有变量都设置为自动持续时间。
以下是一个auto.cpp
代码,演示了变量中使用auto
关键字。我们将使用auto
关键字定义四个变量,然后使用typeid()
函数找出每个变量的数据类型。让我们来看一下:
/* auto.cpp */
#include <iostream>
#include <typeinfo>
int main()
{
std::cout << "[auto.cpp]" << std::endl;
// Creating several auto-type variables
auto a = 1;
auto b = 1.0;
auto c = a + b;
auto d = {b, c};
// Displaying the preceding variables' type
std::cout << "type of a: " << typeid(a).name() << std::endl;
std::cout << "type of b: " << typeid(b).name() << std::endl;
std::cout << "type of c: " << typeid(c).name() << std::endl;
std::cout << "type of d: " << typeid(d).name() << std::endl;
return 0;
}
正如我们在前面的代码中看到的,我们有一个将存储整数
值的变量a
,并且有一个将存储双精度
值的变量b
。我们计算a
和b
的加法,并将结果存储在变量c
中。在这里,我们期望c
将存储双精度
对象,因为我们添加了整数
和双精度
对象。最后是将存储initializer_list<double>
数据类型的变量d
。当我们运行前面的代码时,将在控制台上看到以下输出:
如前面的快照所示,我们只给出了数据类型的第一个字符,比如i
代表整数
,d
代表双精度
,St16initializer_listIdE
代表initializer_list<double>
,最后一个小写的d
字符代表双精度
。
我们可能需要在编译器选项中启用运行时类型信息(RTTI)功能来检索数据类型对象。然而,GCC 已经默认启用了这个功能。此外,typeid()
函数的使用输出取决于编译器。我们可能会得到原始类型名称,或者就像我们在前面的例子中所做的那样,只是一个符号。
此外,对于变量,正如我们之前讨论的那样,auto
关键字也可以应用于函数,自动推断函数的返回类型。假设我们有以下名为add()
的简单函数来计算两个参数的加法:
int add(int i, int j)
{
return i + j;
}
我们可以重构前面的方法来使用auto
关键字,如下所示的代码行:
auto add(int i, int j)
{
return i + j;
}
与自动类型变量类似,编译器可以根据函数的返回值决定正确的返回类型。正如前面的代码所示,该函数确实返回整数值,因为我们只是添加了两个整数值。
现代 C++中使用auto
关键字的另一个特性是尾返回类型语法。通过使用这个特性,我们可以指定返回类型,函数原型的其余部分,或函数签名。从前面的代码中,我们可以重构它以使用以下特性:
auto add(int i, int j) -> int
{
return i + j;
}
你可能会问我为什么我们在箭头符号(->
)之后再次指定数据类型,即使我们已经使用了auto
关键字。当我们在下一节讨论decltype
关键字时,我们将找到答案。此外,通过使用这个特性,我们现在可以通过修改main()
方法的语法来稍微重构前面的auto.cpp
代码,而不是main()
函数签名的以下语法:
int main()
{
// The body of the function
}
我们可以将签名语法改为以下代码行:
auto main -> int
{
// The body of the function
}
现在,我们将看到本书中的所有代码都使用这个尾返回类型特性来应用现代 C++语法。
使用 decltype 关键字查询表达式的类型
我们在前面的部分讨论了auto
关键字可以根据其存储的值的类型自动推断变量的类型。该关键字还可以根据其返回值的类型推断函数的返回类型。现在,让我们结合auto
关键字和decltype
关键字,获得现代 C++的功能。
在我们结合这两个关键字之前,我们将找出decltype
关键字的用途--它用于询问对象或表达式的类型。让我们看一下以下几行简单的变量声明:
const int func1();
const int& func2();
int i;
struct X { double d; };
const X* x = new X();
现在,基于前面的代码,我们可以使用decltype
关键字声明其他变量,如下所示:
// Declaring const int variable
// using func1() type
decltype(func1()) f1;
// Declaring const int& variable
// using func2() type
decltype(func2()) f2;
// Declaring int variable
// using i type
decltype(i) i1;
// Declaring double variable
// using struct X type
decltype(x->d) d1; // type is double
decltype((x->d)) d2; // type is const double&
正如我们在前面的代码中所看到的,我们可以根据另一个对象的类型指定对象的类型。现在,假设我们需要重构前面的add()
方法成为一个模板。没有auto
和decltype
关键字,我们将有以下模板实现:
template<typename I, typename J, typename K>
K add(I i, J j)
{
return i + j;
}
幸运的是,由于auto
关键字可以指定函数的返回类型,即尾返回类型,而decltype
关键字可以根据表达式推断类型,我们可以将前面的模板重构如下:
template<typename I, typename J>
auto add(I i, J j) -> decltype(i + j)
{
return i + j;
}
为了证明,让我们编译和运行以下的decltype.cpp
代码。我们将使用以下模板来计算两种不同值类型--整数
和双精度
的加法:
/* decltype.cpp */
#include <iostream>
// Creating template
template<typename I, typename J>
auto add(I i, J j) -> decltype(i + j)
{
return i + j;
}
auto main() -> int
{
std::cout << "[decltype.cpp]" << std::endl;
// Consuming the template
auto d = add<int, double>(2, 2.5);
// Displaying the preceding variables' type
std::cout << "result of 2 + 2.5: " << d << std::endl;
return 0;
}
编译过程应该可以顺利进行,没有错误。如果我们运行前面的代码,我们将在屏幕上看到以下输出:
正如我们所看到的,我们成功地结合了auto
和decltype
关键字,创建了一个比现代 C++宣布之前通常更简单的模板。
指向空指针
现代 C++中的另一个新功能是一个名为nullptr
的关键字,它取代了NULL
宏来表示空指针。现在,在使用NULL
宏表示零数字或空指针时不再存在歧义。假设我们在声明中有以下两个方法的签名:
void funct(const char *);
void funct(int)
前一个函数将传递一个指针作为参数,后一个将传递整数作为参数。然后,我们调用funct()
方法并将NULL
宏作为参数传递,如下所示:
funct(NULL);
我们打算调用前一个函数。然而,由于我们传递了NULL
参数,它基本上被定义为0
,后一个函数将被调用。在现代 C++中,我们可以使用nullptr
关键字来确保我们将传递一个空指针给参数。调用funct()
方法应该如下:
funct(nullptr);
现在编译器将调用前一个函数,因为它将一个空指针传递给参数,这是我们期望的。不再存在歧义,将避免不必要的未来问题。
使用非成员 begin()和 end()函数返回迭代器
在现代 C++之前,要迭代一个序列,我们需要调用每个容器的begin()
和end()
成员方法。对于数组,我们可以通过迭代索引来迭代它的元素。自 C++11 以来,语言有一个非成员函数--begin()
和end()
--来检索序列的迭代器。假设我们有以下元素的数组:
int arr[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
当语言没有begin()
和end()
函数时,我们需要使用索引来迭代数组的元素,可以在下面的代码行中看到:
for (unsigned int i = 0; i < sizeof(arr)/sizeof(arr[0]); ++i)
// Do something to the array
幸运的是,使用begin()
和end()
函数,我们可以重构前面的for
循环如下:
for (auto i = std::begin(arr); i != std::end(arr); ++i)
// Do something to the array
正如我们所看到的,使用begin()
和end()
函数创建了一个紧凑的代码,因为我们不需要担心数组的长度,因为begin()
和end()
的迭代器指针会为我们做这件事。为了比较,让我们看一下以下的begin_end.cpp
代码:
/* begin_end.cpp */
#include <iostream>
auto main() -> int
{
std::cout << "[begin_end.cpp]" << std::endl;
// Declaring an array
int arr[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// Displaying the array elements
// using conventional for-loop
std::cout << "Displaying array element using conventional for-
loop";
std::cout << std::endl;
for (unsigned int i = 0; i < sizeof(arr)/sizeof(arr[0]); ++i)
std::cout << arr[i] << " ";
std::cout << std::endl;
// Displaying the array elements
// using non-member begin() and end()
std::cout << "Displaying array element using non-member begin()
and end()";
std::cout << std::endl;
for (auto i = std::begin(arr); i != std::end(arr); ++i)
std::cout << *i << " ";
std::cout << std::endl;
return 0;
}
为了证明前面的代码,我们可以编译代码,当我们运行它时,应该在控制台屏幕上显示以下输出:
正如我们在屏幕截图中看到的,当我们使用传统的for-loop
或begin()
和end()
函数时,我们得到了完全相同的输出。
使用基于范围的 for 循环迭代集合
在现代 C++中,有一个新功能被增强,支持for-each
技术来迭代集合。如果你想对集合或数组的元素做一些操作而不关心元素的数量或索引,这个功能就很有用。这个功能的语法也很简单。假设我们有一个名为arr
的数组,我们想要使用range-based for loop
技术迭代每个元素,我们可以使用以下语法:
for (auto a : arr)
// Do something with a
因此,我们可以重构我们之前的begin_end.cpp
代码,使用range-based for loop
,如下所示:
/* range_based_for_loop.cpp */
#include <iostream>
auto main() -> int
{
std::cout << "[range_based_for_loop.cpp]" << std::endl;
// Declaring an array
int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
// Displaying the array elements
// using non-member begin() and end()
std::cout << "Displaying array element using range-based for
loop";
std::cout << std::endl;
for (auto a : arr) std::cout << a << " ";
std::cout << std::endl;
return 0;
}
我们在前面的代码中看到的语法现在更简单了。如果我们编译前面的代码,应该不会出现错误,如果我们运行代码,应该在控制台屏幕上看到以下输出:
现在我们有了一种新的技术来迭代集合,而不必关心集合的索引。我们将在本书中继续使用它。
利用 C++语言与 C++标准库
C++标准库是一个强大的类和函数集合,具有创建应用程序所需的许多功能。它们由 C++ ISO 标准委员会控制,并受到标准模板库(STL)的影响,在 C++11 引入之前是通用库。标准库中的所有功能都在std 命名空间
中声明,不再以.h
结尾的头文件(除了 18 个 ISO C90 C 标准库的头文件,它们被合并到了 C++标准库中)。
C++标准库中包含了几个头文件,其中包含了 C++标准库的声明。然而,在这些小章节中几乎不可能讨论所有的头文件。因此,我们将讨论一些我们在日常编码活动中最常使用的功能。
将任何对象放入容器中
容器是用来存储其他对象并管理它所包含的对象的内存的对象。数组是 C++11 中添加的一个新特性,用于存储特定数据类型的集合。它是一个序列容器,因为它存储相同数据类型的对象并将它们线性排列。让我们看一下以下代码片段:
/* array.cpp */
#include <array>
#include <iostream>
auto main() -> int
{
std::cout << "[array.cpp]" << std::endl;
// Initializing an array containing five integer elements
std::array<int, 10> arr = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// Displaying the original elements of the array
std::cout << "Original Data : ";
for(auto a : arr) std::cout << a << " ";
std::cout << std::endl;
// Modifying the content of
// the 1st and 3rd element of the array
arr[1] = 9;
arr[3] = 7;
// Displaying the altered array elements
std::cout << "Manipulated Data: ";
for(auto a : arr) std::cout << a << " ";
std::cout << std::endl;
return 0;
}
正如我们在前面的代码中所看到的,我们实例化了一个名为arr
的新数组,将其长度设置为10
,并且只批准int
元素。我们可以猜到,代码的输出是一行数字0
到9
,显示了原始数据,另一行将显示更改后的数据,如我们在以下截图中所看到的:
如果我们使用std::array
声明一个数组,就不会有性能问题;我们在array.cpp
代码中使用它,并将其与在begin_end.cpp
代码中使用的普通数组进行比较。然而,在现代 C++中,我们有一个新的数组声明,它具有友好的值语义,因此可以按值传递给函数或从函数中返回。此外,这个新数组声明的接口使得更方便地找到大小,并与标准模板库(STL)风格的基于迭代器的算法一起使用。
使用数组作为容器是很好的,因为我们可以存储数据并对其进行操作。我们还可以对其进行排序,并查找特定元素。然而,由于数组是一个在编译时不可调整大小的对象,我们必须在最开始决定要使用的数组的大小,因为我们不能后来改变大小。换句话说,我们不能在现有数组中插入或删除元素。作为解决这个问题的方法,以及为了最佳实践使用容器,我们现在可以使用vector
来存储我们的集合。让我们看一下以下代码:
/* vector.cpp */
#include <vector>
#include <iostream>
auto main() -> int
{
std::cout << "[vector.cpp]" << std::endl;
// Initializing a vector containing three integer elements
std::vector<int> vect = { 0, 1, 2 };
// Displaying the original elements of the vector
std::cout << "Original Data : ";
for (auto v : vect) std::cout << v << " ";
std::cout << std::endl;
// Adding two new data
vect.push_back(3);
vect.push_back(4);
// Displaying the elements of the new vector
// and reverse the order
std::cout << "New Data Added : ";
for (auto v : vect) std::cout << v << " ";
std::cout << std::endl;
// Modifying the content of
// the 2nd and 4th element of the vector
vect.at(2) = 5;
vect.at(4) = 6;
// Displaying the altered array elements
std::cout << "Manipulate Data: ";
for (auto v : vect) std::cout << v << " ";
std::cout << std::endl;
return 0;
}
现在,在我们之前的代码中有一个vector
实例,而不是一个array
实例。正如我们所看到的,我们使用push_back()
方法为vector
实例添加了一个额外的值。我们可以随时添加值。每个元素的操作也更容易,因为vector
有一个at()
方法,它返回特定索引的元素的引用。运行代码时,我们将看到以下截图作为输出:
当我们想要通过索引访问vector
实例中的特定元素时,最好始终使用at()
方法而不是[]
运算符。这是因为,当我们意外地访问超出范围的位置时,at()
方法将抛出一个out_of_range
异常。否则,[]
运算符将产生未定义的行为。
使用算法
我们可以对array
或vector
中的集合元素进行排序,以及查找特定内容的元素。为了实现这些目的,我们必须使用 C++标准库提供的算法功能。让我们看一下以下代码,以演示算法功能中排序元素的能力:
/* sort.cpp */
#include <vector>
#include <algorithm>
#include <iostream>
bool comparer(int a, int b)
{
return (a > b);
}
auto main() -> int
{
std::cout << "[sort.cpp]" << std::endl;
// Initializing a vector containing several integer elements
std::vector<int> vect = { 20, 43, 11, 78, 5, 96 };
// Displaying the original elements of the vector
std::cout << "Original Data : ";
for (auto v : vect)
std::cout << v << " ";
std::cout << std::endl;
// Sorting the vector element ascending
std::sort(std::begin(vect), std::end(vect));
// Displaying the ascending sorted elements
// of the vector
std::cout << "Ascending Sorted : ";
for (auto v : vect)
std::cout << v << " ";
std::cout << std::endl;
// Sorting the vector element descending
// using comparer
std::sort(std::begin(vect), std::end(vect), comparer);
// Displaying the descending sorted elements
// of the vector
std::cout << "Descending Sorted: ";
for (auto v : vect)
std::cout << v << " ";
std::cout << std::endl;
return 0;
}
正如我们在前面的代码中看到的,我们两次调用了sort()
方法。首先,我们只提供了我们想要排序的元素的范围。然后,我们添加了比较函数comparer()
,以便将其提供给sort()
方法,以获得更多灵活性。从前面的代码中,我们将在控制台上看到的输出如下:
从前面的截图中,我们可以看到一开始vector
中有六个元素。然后,我们使用简单的sort()
方法对向量的元素进行排序。然后,我们再次调用sort()
方法,但现在不是简单的sort()
方法,而是将comparer()
提供给sort()
方法。结果,向量元素将按降序排序,因为comparer()
函数从两个输入中寻找更大的值。
现在,让我们转向算法特性具有的另一个功能,即查找特定元素。假设我们在代码中有Vehicle
类。它有两个名为m_vehicleType
和m_totalOfWheel
的私有字段,我们可以从 getter 方法GetType()
和GetNumOfWheel()
中检索值。它还有两个构造函数,分别是默认构造函数和用户定义的构造函数。类的声明应该如下所示:
/* vehicle.h */
#ifndef __VEHICLE_H__
#define __VEHICLE_H__
#include <string>
class Vehicle
{
private:
std::string vehicleType;
int totalOfWheel;
public:
Vehicle(
const std::string &type,
int _wheel);
Vehicle();
~Vehicle();
std::string GetType() const {return vehicleType;}
int GetNumOfWheel() const {return totalOfWheel;}
};
#endif // End of __VEHICLE_H__
Vehicle
类的实现如下:
/* vehicle.cpp */
#include "vehicle.h"
using namespace std;
// Constructor with default value for
// m_vehicleType and m_totalOfWheel
Vehicle::Vehicle() : m_totalOfWheel(0)
{
}
// Constructor with user-defined value for
// m_vehicleType and m_totalOfWheel
Vehicle::Vehicle( const string &type, int wheel) :
m_vehicleType(type),
m_totalOfWheel(wheel)
{
}
// Destructor
Vehicle::~Vehicle()
{
}
我们将在vector
容器中存储一组Vehicle
,然后根据其属性搜索一些元素。代码如下:
/* find.cpp */
#include <vector>
#include <algorithm>
#include <iostream>
#include "../vehicle/vehicle.h"
using namespace std;
bool TwoWheeled(const Vehicle &vehicle)
{
return _vehicle.GetNumOfWheel() == 2 ?
true : false;
}
auto main() -> int
{
cout << "[find.cpp]" << endl;
// Initializing several Vehicle instances
Vehicle car("car", 4);
Vehicle motorcycle("motorcycle", 2);
Vehicle bicycle("bicycle", 2);
Vehicle bus("bus", 6);
// Assigning the preceding Vehicle instances to a vector
vector<Vehicle> vehicles = { car, motorcycle, bicycle, bus };
// Displaying the elements of the vector
cout << "All vehicles:" << endl;;
for (auto v : vehicles)
std::cout << v.GetType() << endl;
cout << endl;
// Displaying the elements of the vector
// which are the two-wheeled vehicles
cout << "Two-wheeled vehicle(s):" << endl;;
auto tw = find_if(
begin(vehicles),
end(vehicles),
TwoWheeled);
while (tw != end(vehicles))
{
cout << tw->GetType() << endl ;
tw = find_if(++tw, end(vehicles), TwoWheeled);
}
cout << endl;
// Displaying the elements of the vector
// which are not the two-wheeled vehicles
cout << "Not the two-wheeled vehicle(s):" << endl;;
auto ntw = find_if_not(begin(vehicles),
end(vehicles),
TwoWheeled);
while (ntw != end(vehicles))
{
cout << ntw->GetType() << endl ;
ntw = find_if_not(++ntw, end(vehicles), TwoWheeled);
}
return 0;
}
正如我们所看到的,我们实例化了四个Vehicle
对象,然后将它们存储在vector
中。在那里,我们试图找到有两个轮子的车辆。find_if()
函数用于此目的。我们还有TwoWheeled()
方法来提供比较值。由于我们正在寻找两轮车辆,我们将通过调用GetNumOfWheel()
方法来检查Vehicle
类中的totalOfWheel
变量。相反,如果我们想找到不符合比较值的元素,我们可以使用在 C++11 中添加的find_if_not()
函数。我们得到的输出应该是这样的:
正如我们在vehicle.cpp
代码和find.cpp
代码中看到的,我们现在在*.cpp
文件中添加了using namespace std;
行。我们这样做是为了使我们的编码活动变得更加高效,因为我们不必输入太多的单词。相反,在vehicle.h
中,我们仍然使用std::
后跟方法或属性名称,而不是在开头使用 std 命名空间。在头文件中最好不要声明using namespace
,因为头文件是我们将为实例创建一些库时要交付的文件。我们库的用户可能有与我们的库具有相同名称的函数。这肯定会在这两个函数之间创建冲突。
我们将最常使用的另一个算法特性是for_each
循环。使用for_each
循环而不是使用for
循环,在许多情况下会使我们的代码更简洁。它也比for
循环更简单,更不容易出错,因为我们可以为for_each
循环定义一个特定的函数。现在让我们重构我们之前的代码以使用for_each
循环。代码如下所示:
/* for_each.cpp */
#include <vector>
#include <algorithm>
#include <iostream>
#include "vehicle.h"
using namespace std;
void PrintOut(const Vehicle &vehicle)
{
cout << vehicle.GetType() << endl;
}
auto main() -> int
{
cout << "[for_each.cpp]" << endl;
// Initializing several Vehicle instances
Vehicle car("car", 4);
Vehicle motorcycle("motorcycle", 2);
Vehicle bicycle("bicycle", 2);
Vehicle bus("bus", 6);
// Assigning the preceding Vehicle instances to a vector
vector<Vehicle> vehicles = { car, motorcycle, bicycle, bus };
// Displaying the elements of the vector
cout << "All vehicles:" << endl;
for_each(begin(vehicles), end(vehicles), PrintOut);
return 0;
}
现在,使用for_each
循环,我们有了更清晰的代码。我们只需要提供第一个和最后一个迭代器,然后传递一个函数--在这种情况下是PrintOut()
函数--它将在范围内的每个元素中被调用。
使用 Lambda 表达式简化函数表示
Lambda 表达式是一个匿名符号,表示执行操作或计算的东西。在函数式编程中,Lambda 表达式对于生成第一类和纯函数非常有用,我们将在本书的不同章节中讨论。现在,让我们通过研究 Lambda 表达式的三个基本部分来熟悉 C++11 中引入的这个新特性。
-
捕获列表: []
-
参数列表: ()
-
主体: {}
这三个基本部分的顺序如下:
[](){}
捕获列表部分也用作标记来识别 Lambda 表达式。它是一个占位符,用于在表达式中涉及的值。唯一的捕获默认值是和符号(&
),它将隐式地通过引用捕获自动变量,以及等号(=
),它将隐式地通过复制捕获自动变量(我们将在接下来的部分进一步讨论)。参数列表类似于每个函数中的捕获列表,我们可以向其传递值。主体是函数本身的实现。
使用 Lambda 表达式进行微小函数
想象我们有一个只调用一次的微小单行函数。最好的做法是在需要时直接编写该函数的操作。实际上,在我们讨论 C++标准库时,我们在之前的示例中就有这样的函数。只需返回到for_each.cpp
文件,我们将找到for_each()
仅调用一次的PrintOut()
函数。如果我们使用 Lambda,可以使这个for_each
循环更易读。让我们看一下以下代码片段,以查看我们如何重构for_each.cpp
文件:
/* lambda_tiny_func.cpp */
#include <vector>
#include <algorithm>
#include <iostream>
#include "../vehicle/vehicle.h"
using namespace std;
auto main() -> int
{
cout << "[lambda_tiny_func.cpp]" << endl;
// Initializing several Vehicle instances
Vehicle car("car", 4);
Vehicle motorcycle("motorcycle", 2);
Vehicle bicycle("bicycle", 2);
Vehicle bus("bus", 6);
// Assigning the preceding Vehicle instances to a vector
vector<Vehicle> vehicles = { car, motorcycle, bicycle, bus };
// Displaying the elements of the vector
// using Lambda expression
cout << "All vehicles:" << endl;
for_each(
begin(vehicles),
end(vehicles),
[](const Vehicle &vehicle){
cout << vehicle.GetType() << endl;
});
return 0;
}
如我们所见,我们已经将在for_each.cpp
文件中使用的PrintOut()
函数转换为 Lambda 表达式,并将其传递给for_each
循环。它确实会产生与for_each.cpp
文件相同的输出。但是,现在我们的代码变得更加简洁和易读。
用于多行函数的 Lambda 表达式
Lambda 表达式也可以用于多行函数,因此我们可以将函数的主体放在其中。这将使我们的代码更易读。让我们编写一个新代码。在该代码中,我们将有一个整数集合和一个意图来检查所选元素是否为质数。我们可以创建一个单独的函数,例如PrintPrime()
,然后调用它。但是,由于质数检查操作只调用一次,如果我们将其转换为 Lambda 表达式,代码会更易读。代码应该如下所示:
/* lambda_multiline_func.cpp */
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[lambda_multiline_func.cpp]" << endl;
// Initializing a vector containing integer element
vector<int> vect;
for (int i = 0; i < 10; ++i)
vect.push_back(i);
// Displaying whether or not the element is prime number
for_each(
begin(vect),
end(vect),
[](int n) {
cout << n << " is";
if(n < 2)
{
if(n == 0)
cout << " not";
}
else
{
for (int j = 2; j < n; ++j)
{
if (n % j == 0)
{
cout << " not";
break;
}
}
}
cout << " prime number" << endl;
});
return 0;
}
我们应该在屏幕上看到的输出如下:
如我们在前面的屏幕截图中所见,我们已成功使用 Lambda 表达式识别了质数。
从 Lambda 表达式返回值
我们之前的两个 Lambda 表达式示例只是用于在控制台上打印。这意味着函数不需要返回任何值。但是,我们可以要求 Lambda 表达式返回一个值,例如,如果我们在函数内部进行计算并返回计算结果。让我们看一下以下代码,以查看如何使用这个 Lambda:
/* lambda_returning_value.cpp */
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[lambda_returning_value.cpp]" << endl;
// Initializing a vector containing integer element
vector<int> vect;
for (int i = 0; i < 10; ++i)
vect.push_back(i);
// Displaying the elements of vect
cout << "Original Data:" << endl;
for_each(
begin(vect),
end(vect),
[](int n){
cout << n << " ";
});
cout << endl;
// Creating another vect2 vector
vector<int> vect2;
// Resize the size of vect2 exactly same with vect
vect2.resize(vect.size());
// Doubling the elements of vect and store to vect2
transform(
begin(vect),
end(vect),
begin(vect2),
[](int n) {
return n * n;
});
// Displaying the elements of vect2
cout << "Squared Data:" << endl;
for_each(
begin(vect2),
end(vect2),
[](int n) {
cout << n << " ";
});
cout << endl;
// Creating another vect3 vector
vector<double> vect3;
// Resize the size of vect3 exactly same with vect
vect3.resize(vect.size());
// Finding the average of the elements of vect
// and store to vect2
transform(
begin(vect2),
end(vect2),
begin(vect3),
[](int n) -> double {
return n / 2.0;
});
// Displaying the elements of vect3
cout << "Average Data:" << endl;
for_each(
begin(vect3),
end(vect3),
[](double d) {
cout << d << " ";
});
cout << endl;
return 0;
}
当我们在前面的代码中使用transform()
方法时,我们有一个 Lambda 表达式,它从n * n
的计算中返回一个值。但是,表达式中没有声明返回类型。这是因为我们可以省略返回类型的声明,因为编译器已经理解到表达式将返回一个整数
值。因此,在我们有另一个与vect
大小相同的向量vect2
之后,我们可以调用transform()
方法以及 Lambda 表达式,vect
的值将加倍并存储在vect2
中。
如果我们愿意,我们可以为 Lambda 表达式指定返回类型。正如我们在前面的代码中所看到的,我们根据vect
向量的所有值转换了vect3
向量,但现在我们使用箭头符号(->
)指定了返回类型为double
。前面代码的结果应该如下截图所示:
正如我们从前面的截图中所看到的,我们已经成功地使用 Lambda 表达式找到了加倍和平均值的结果。
捕获值到 Lambda 表达式
在我们之前的 Lambda 表达式示例中,我们保持了捕获部分和方括号([]
)为空,因为 Lambda 没有捕获任何东西,并且在编译器生成的匿名对象中没有任何额外的成员变量。我们还可以通过在这个方括号中指定要捕获的对象来指定我们想要在 Lambda 表达式中捕获的对象。让我们看一下以下代码片段来讨论一下:
/* lambda_capturing_by_value.cpp */
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[lambda_capturing_by_value.cpp]" << endl;
// Initializing a vector containing integer element
vector<int> vect;
for (int i = 0; i < 10; ++i)
vect.push_back(i);
// Displaying the elements of vect
cout << "Original Data:" << endl;
for_each(
begin(vect),
end(vect),
[](int n){
cout << n << " ";
});
cout << endl;
// Initializing two variables
int a = 2;
int b = 8;
// Capturing value explicitly from the two variables
cout << "Printing elements between " << a;
cout << " and " << b << " explicitly [a,b]:" << endl;
for_each(
begin(vect),
end(vect),
a,b{
if (n >= a && n <= b)
cout << n << " ";
});
cout << endl;
// Modifying variable a and b
a = 3;
b = 7;
// Capturing value implicitly from the two variables
cout << "printing elements between " << a;
cout << " and " << b << " implicitly[=]:" << endl;
for_each(
begin(vect),
end(vect),
={
if (n >= a && n <= b)
cout << n << " ";
});
cout << endl;
return 0;
}
在前面的代码中,我们将尝试在 Lambda 表达式中显式和隐式地捕获值。假设我们有两个变量a
和b
,我们想要显式地捕获这些值,我们可以在 Lambda 表达式中使用[a,b]
语句指定它们,然后在函数体内部使用这些值。此外,如果我们希望隐式地捕获值,只需使用[=]
作为捕获部分,然后表达式将知道我们在函数体中指定的变量。如果我们运行前面的代码,我们将在屏幕上得到以下输出:
我们还可以改变我们捕获的值的状态,而不修改 Lambda 表达式函数体外的值。为此,我们可以使用与之前相同的技术,并在以下代码块中添加mutable
关键字:
/* lambda_capturing_by_value_mutable.cpp */
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[lambda_capturing_by_value_mutable.cpp]" << endl;
// Initializing a vector containing integer element
vector<int> vect;
for (int i = 0; i < 10; ++i)
vect.push_back(i);
// Displaying the elements of vect
cout << "Original Data:" << endl;
for_each(
begin(vect),
end(vect),
[](int n){
cout << n << " ";
});
cout << endl;
// Initializing two variables
int a = 1;
int b = 1;
// Capturing value from the two variables
// without mutate them
for_each(
begin(vect),
end(vect),
= mutable {
const int old = x;
x *= 2;
a = b;
b = old;
});
// Displaying the elements of vect
cout << "Squared Data:" << endl;
for_each(
begin(vect),
end(vect),
[](int n) {
cout << n << " ";
});
cout << endl << endl;
// Displaying value of variable a and b
cout << "a = " << a << endl;
cout << "b = " << b << endl;
return 0;
}
前面的代码将对vect
向量的元素进行加倍。它在 Lambda 表达式中使用值捕获,并且使用了mutable
关键字。正如我们所看到的,我们通过引用传递了向量元素(int& x)
并将其乘以 2,然后改变了a
和b
的值。然而,由于我们使用了mutable
关键字,a
和b
的最终结果将保持不变,尽管我们已经通过引用传递了向量。控制台上的输出如下截图所示:
如果我们想要改变a
和b
变量的值,我们必须使用 Lambda 表达式通过引用进行捕获。我们可以通过在 Lambda 表达式的尖括号中传递引用来实现这一点,例如[&a, &b]
。更多细节,请看以下代码片段:
/* lambda_capturing_by_reference.cpp */
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[lambda_capturing_by_reference.cpp]" << endl;
// Initializing a vector containing integer element
vector<int> vect;
for (int i = 0; i < 10; ++i)
vect.push_back(i);
// Displaying the elements of vect
cout << "Original Data:" << endl;
for_each(
begin(vect),
end(vect),
[](int n){
cout << n << " ";
});
cout << endl;
// Initializing two variables
int a = 1;
int b = 1;
// Capturing value from the two variables
// and mutate them
for_each(
begin(vect),
end(vect),
&a, &b{
const int old = x;
x *= 2;
a = b;
b = old;
});
// Displaying the elements of vect
cout << "Squared Data:" << endl;
for_each(
begin(vect),
end(vect),
[](int n) {
cout << n << " ";
});
cout << endl << endl;
// Displaying value of variable a and b
cout << "a = " << a << endl;
cout << "b = " << b << endl;
return 0;
}
前面的代码与lambda_capturing_by_value_mutable.cpp
文件具有相同的行为,它将对vect
向量的元素进行加倍。然而,通过引用捕获,它现在在for_each
循环中处理a
和b
的值时也会修改它们。a
和b
的值将在代码结束时被改变,正如我们在以下截图中所看到的:
使用初始化捕获准备值
C++14 中 Lambda 表达式的另一个重要特性是其初始化捕获。该表达式可以捕获变量的值并将其赋给表达式的变量。让我们看一下以下实现初始化捕获的代码片段:
/* lambda_initialization_captures.cpp */
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[lambda_initialization_captures.cpp]" << endl;
// Initializing a variable
int a = 5;
cout << "Initial a = " << a << endl;
// Initializing value to lambda using the variable
auto myLambda = [&x = a]() { x += 2; };
// Executing the Lambda
myLambda();
// Displaying a new value of the variable
cout << "New a = " << a << endl;
return 0;
}
正如我们在前面的代码中所看到的,我们有一个名为a
的 int 变量,其值为5
。然后,Lambda 表达式myLambda
捕获了a
的值并在代码中执行。结果是现在a
的值将是7
,因为它被加上了2
。当我们运行前面的代码时,以下输出截图应该出现在我们的控制台窗口中:
从上面的快照中,我们可以看到可以在 Lambda 表达式中准备要包含在计算中的值。
编写一个通用的 Lambda 表达式,可以多次使用,适用于许多不同的数据类型
在 C++14 之前,我们必须明确声明参数列表的类型。幸运的是,现在在 C++14 中,Lambda 表达式接受auto
作为有效的参数类型。因此,我们现在可以构建一个通用的 Lambda 表达式,如下面的代码所示。在该代码中,我们只有一个 Lambda 表达式,用于找出传递给表达式的两个数字中的最大值。我们将在参数声明中使用auto
关键字,以便可以传递任何数据类型。因此,findMax()
函数的参数可以传递int
和float
数据类型。代码应该如下所示:
/* lambda_expression_generic.cpp */
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[lambda_expression_generic.cpp]" << endl;
// Creating a generic lambda expression
auto findMax = [](auto &x, auto &y){
return x > y ? x : y; };
// Initializing various variables
int i1 = 5, i2 = 3;
float f1 = 2.5f, f2 = 2.05f;
// Consuming generic lambda expression
// using integer data type
cout << "i1 = 5, i2 = 3" << endl;
cout << "Max: " << findMax(i1, i2) << endl << endl;
// Consuming generic lambda expression
// using double data type
cout << "f1 = 2.5f, f2 = 2.05f" << endl;
cout << "Max: " << findMax(f1, f2) << endl << endl;
return 0;
}
我们在控制台上看到的输出应该如下所示:
C++17 语言计划为 Lambda 表达式引入两个新特性--它们是捕获*this
,允许表达式通过复制捕获封闭对象,以及constexpr
Lambda 表达式,允许我们在编译时使用 Lambda 表达式的结果并生成constexpr
对象。然而,由于 C++17 尚未发布,我们现在无法尝试它。
使用智能指针避免手动内存管理
智能指针在使用 C++时非常有用,并且对于高效使用 C++来说具有重要的知识。C++11 在memory
头文件中添加了许多智能指针的新功能。在 C++11 之前的很长一段时间里,我们使用auto_ptr
作为智能指针。然而,它相当不安全,因为它具有不兼容的复制语义。它现在也已经被弃用,我们不应该再使用它。幸运的是,C++引入了unique_ptr
,它具有类似的功能,但具有额外的功能,例如添加deleters
和对数组的支持。我们可以用unique_ptr
做任何auto_ptr
能做的事情,而且应该使用unique_ptr
来代替。我们将深入讨论unique_ptr
以及 C++11 中的其他新智能指针--shared_ptr
和weak_ptr
。
使用 unique_ptr 替换原始指针
接下来我们将看到的指针是unique_ptr
指针。它快速、高效,并且几乎可以直接替换原始指针。它提供独占所有权语义,独占它指向的对象。由于其独占性,如果它具有非空指针,则可以在其析构函数被调用时销毁对象。由于其独占性,它也不能被复制。它没有复制构造函数和复制赋值。尽管它不能被复制,但它可以被移动,因为它提供了移动构造函数和移动赋值。
这些是我们可以用来构造unique_ptr
的方法:
auto up1 = unique_ptr<int>{};
auto up2 = unique_ptr<int>{ nullptr };
auto up3 = unique_ptr<int>{ new int { 1234 } };
根据上述代码,up1
和up2
将构造指向空(null)的两个新的unique_ptr
,而up3
将指向保存1234
值的地址。然而,C++14 添加了一个新的库函数来构造unique_ptr
,即make_unique
。因此,我们可以按照以下方式构造一个新的unique_ptr
指针:
auto up4 = make_unique<int>(1234);
up4
变量也将指向保存1234
值的地址。
现在,让我们看一下以下代码块:
/* unique_ptr_1.cpp */
#include <memory>
#include <iostream>
using namespace std;
struct BodyMass
{
int Id;
float Weight;
BodyMass(int id, float weight) :
Id(id),
Weight(weight)
{
cout << "BodyMass is constructed!" << endl;
cout << "Id = " << Id << endl;
cout << "Weight = " << Weight << endl;
}
~BodyMass()
{
cout << "BodyMass is destructed!" << endl;
}
};
auto main() -> int
{
cout << "[unique_ptr_1.cpp]" << endl;
auto myWeight = make_unique<BodyMass>(1, 165.3f);
cout << endl << "Doing something!!!" << endl << endl;
return 0;
}
我们尝试构造一个新的unique_ptr
指针,该指针指向保存BodyMass
数据类型的地址。在BodyMass
中,我们有一个构造函数和一个析构函数。现在,让我们通过运行上述代码来看看unique_ptr
指针是如何工作的。我们在屏幕上得到的输出应该如下截图所示:
正如我们在前面的截图中看到的,当我们构造unique_ptr
时,构造函数被调用。此外,与传统的 C++语言不同,我们在使用指针时必须释放内存,而在现代 C++中,当超出范围时,内存将自动释放。我们可以看到,当程序退出时,BodyMass
的析构函数被调用,这意味着myWeight
已经超出范围。
现在,让我们通过分析以下代码片段来测试unique_ptr
的独占性:
/* unique_ptr_2.cpp */
#include <memory>
#include <iostream>
using namespace std;
struct BodyMass
{
int Id;
float Weight;
BodyMass(int id, float weight) :
Id(id),
Weight(weight)
{
cout << "BodyMass is constructed!" << endl;
cout << "Id = " << Id << endl;
cout << "Weight = " << Weight << endl;
}
BodyMass(const BodyMass &other) :
Id(other.Id),
Weight(other.Weight)
{
cout << "BodyMass is copy constructed!" << endl;
cout << "Id = " << Id << endl;
cout << "Weight = " << Weight << endl;
}
~BodyMass()
{
cout << "BodyMass is destructed!" << endl;
}
};
auto main() -> int
{
cout << "[unique_ptr_2.cpp]" << endl;
auto myWeight = make_unique<BodyMass>(1, 165.3f);
// The compiler will forbid to create another pointer
// that points to the same allocated memory/object
// since it's unique pointer
//auto myWeight2 = myWeight;
// However, we can do the following expression
// since it actually copies the object that has been allocated
// (not the unique_pointer)
auto copyWeight = *myWeight;
return 0;
}
正如我们在前面的代码中看到的,我们不能将unique_ptr
实例分配给另一个指针,因为这将破坏unique_ptr
的独占性。如果我们执行以下表达式,编译器将抛出错误:
auto myWeight2 = myWeight;
然而,我们可以将unique_ptr
的值分配给另一个对象,因为它已经被分配。为了证明这一点,我们已经添加了一个复制构造函数来记录当执行以下表达式时:
auto copyWeight = *myWeight;
如果我们运行前面的unique_ptr_2.cpp
代码,我们将在屏幕上看到以下输出:
正如我们在前面的截图中看到的,当执行复制赋值时,复制构造函数被调用。这证明了我们可以复制unique_ptr
对象的值,但不能复制对象本身。
正如我们之前讨论的,unique_ptr
已经移动了构造函数,尽管它没有复制构造函数。这种构造的使用可以在以下代码片段中找到:
/* unique_ptr_3.cpp */
#include <memory>
#include <iostream>
using namespace std;
struct BodyMass
{
int Id;
float Weight;
BodyMass(int id, float weight) :
Id(id),
Weight(weight)
{
cout << "BodyMass is constructed!" << endl;
cout << "Id = " << Id << endl;
cout << "Weight = " << Weight << endl;
}
~BodyMass()
{
cout << "BodyMass is destructed!" << endl;
}
};
unique_ptr<BodyMass> GetBodyMass()
{
return make_unique<BodyMass>(1, 165.3f);
}
unique_ptr<BodyMass> UpdateBodyMass(
unique_ptr<BodyMass> bodyMass)
{
bodyMass->Weight += 1.0f;
return bodyMass;
}
auto main() -> int
{
cout << "[unique_ptr_3.cpp]" << endl;
auto myWeight = GetBodyMass();
cout << "Current weight = " << myWeight->Weight << endl;
myWeight = UpdateBodyMass(move(myWeight));
cout << "Updated weight = " << myWeight->Weight << endl;
return 0;
}
在前面的代码中,我们有两个新函数--GetBodyMass()
和UpdateBodyMass()
。我们从GetBodyMass()
函数构造一个新的unique_ptr
对象,然后使用UpdateBodyMass()
函数更新其Weight的值。我们可以看到,当我们将参数传递给UpdateBodyMass()
函数时,我们使用move
函数。这是因为unique_ptr
没有复制构造函数,必须移动才能更新其属性的值。前面代码的屏幕输出如下:
使用 shared_ptr 共享对象
与unique_ptr
相比,shared_ptr
实现了共享所有权语义,因此提供了复制构造函数和复制赋值。尽管它们在实现上有所不同,但shared_ptr
实际上是unique_ptr
的计数版本。我们可以调用use_count()
方法来查找shared_ptr
引用的计数值。每个shared_ptr
有效对象的实例被计为一个。我们可以将shared_ptr
实例复制到其他shared_ptr
变量中,引用计数将增加。当销毁shared_ptr
对象时,析构函数会减少引用计数。只有当计数达到零时,对象才会被删除。现在让我们来检查以下shared_ptr
代码:
/* shared_ptr_1.cpp */
#include <memory>
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[shared_ptr_1.cpp]" << endl;
auto sp1 = shared_ptr<int>{};
if(sp1)
cout << "sp1 is initialized" << endl;
else
cout << "sp1 is not initialized" << endl;
cout << "sp1 pointing counter = " << sp1.use_count() << endl;
if(sp1.unique())
cout << "sp1 is unique" << endl;
else
cout << "sp1 is not unique" << endl;
cout << endl;
sp1 = make_shared<int>(1234);
if(sp1)
cout << "sp1 is initialized" << endl;
else
cout << "sp1 is not initialized" << endl;
cout << "sp1 pointing counter = " << sp1.use_count() << endl;
if(sp1.unique())
cout << "sp1 is unique" << endl;
else
cout << "sp1 is not unique" << endl;
cout << endl;
auto sp2 = sp1;
cout << "sp1 pointing counter = " << sp1.use_count() << endl;
if(sp1.unique())
cout << "sp1 is unique" << endl;
else
cout << "sp1 is not unique" << endl;
cout << endl;
cout << "sp2 pointing counter = " << sp2.use_count() << endl;
if(sp2.unique())
cout << "sp2 is unique" << endl;
else
cout << "sp2 is not unique" << endl;
cout << endl;
sp2.reset();
cout << "sp1 pointing counter = " << sp1.use_count() << endl;
if(sp1.unique())
cout << "sp1 is unique" << endl;
else
cout << "sp1 is not unique" << endl;
cout << endl;
return 0;
}
在我们检查前面代码的每一行之前,让我们来看一下应该出现在控制台窗口上的以下输出:
首先,我们创建一个名为sp1
的shared_ptr
对象,但没有实例化它。从控制台上,我们看到sp1
没有被初始化,计数器仍然是0
。它也不是唯一的,因为指针指向了空。然后我们使用make_shared
方法构造sp1
。现在,sp1
被初始化,计数器变为1
。它也变得唯一,因为它是唯一的shared_ptr
对象之一(计数器的值为1
证明了这一点)。接下来,我们创建另一个名为sp2
的变量,并将sp1
复制给它。结果,sp1
和sp2
现在共享相同的对象,计数器和唯一性值证明了这一点。然后,在sp2
中调用reset()
方法将销毁sp2
的对象。现在,sp1
的计数器变为1
,它再次变得唯一。
在shared_ptr_1.cpp
代码中,我们使用shared_ptr<int>
声明unique_ptr
对象,然后调用make_shared<int>
来实例化指针。这是因为我们只需要分析shared_ptr
的行为。然而,我们应该为共享指针使用make_shared<>
,因为它必须在内存中保留引用计数,并且将对象的计数器和内存一起分配,而不是两个单独的分配。
使用 weak_ptr 指针跟踪对象
我们在前面的部分讨论了shared_ptr
。该指针实际上是一个有点胖的指针。它逻辑上指向两个对象,即被管理的对象和使用use_count()
方法的指针计数器。每个shared_ptr
基本上都有一个防止对象被删除的强引用计数和一个不防止对象被删除的弱引用计数,尽管我们甚至没有使用弱引用计数。因此,我们可以只使用一个引用计数,因此我们可以使用weak_ptr
指针。weak_ptr
指针指向由shared_ptr
管理的对象。weak_ptr
的优势在于,它可以用来引用一个对象,但只有在对象仍然存在且不会阻止其他引用持有者删除对象时才能访问它,如果强引用计数达到零。这在处理数据结构时非常有用。让我们看一下下面的代码块,分析weak_ptr
的使用:
/* weak_ptr_1.cpp */
#include <memory>
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[weak_ptr_1.cpp]" << endl;
auto sp = make_shared<int>(1234);
auto wp = weak_ptr<int>{ sp };
if(wp.expired())
cout << "wp is expired" << endl;
else
cout << "wp is not expired" << endl;
cout << "wp pointing counter = " << wp.use_count() << endl;
if(auto locked = wp.lock())
cout << "wp is locked. Value = " << *locked << endl;
else
{
cout << "wp is unlocked" << endl;
wp.reset();
}
cout << endl;
sp = nullptr;
if(wp.expired())
cout << "wp is expired" << endl;
else
cout << "wp is not expired" << endl;
cout << "wp pointing counter = " << wp.use_count() << endl;
if(auto locked = wp.lock())
cout << "wp is locked. Value = " << *locked << endl;
else
{
cout << "wp is unlocked" << endl;
wp.reset();
}
cout << endl;
return 0;
}
在分析前面的代码之前,让我们看一下如果运行代码,从输出控制台中得到的以下截图:
首先,我们实例化shared_ptr
,正如我们之前讨论的,weak_ptr
指向由shared_ptr
管理的对象。然后我们将wp
赋给shared_ptr
变量sp
。有了weak_ptr
指针后,我们可以检查它的行为。通过调用expired()
方法,我们可以判断引用的对象是否已经被删除。由于wp
变量刚刚构造,它还没有过期。weak_ptr
指针还通过调用use_count()
方法来保存对象计数的值,就像我们在shared_ptr
中使用的那样。然后我们调用locked()
方法来创建一个管理引用对象的shared_ptr
,并找到weak_ptr
指向的值。现在我们有一个shared_ptr
变量指向持有1234
值的地址。
然后我们将sp
重置为nullptr
。虽然我们没有触及weak_ptr
指针,但它也发生了变化。从控制台截图中可以看到,现在wp
已经过期,因为对象已被删除。计数器也变为0
,因为它指向了空。此外,它已解锁,因为shared_ptr
对象已被删除。
使用元组存储许多不同的数据类型
我们将熟悉元组,这是一个能够容纳一系列元素的对象,每个元素可以是不同类型的。这是 C++11 中的一个新特性,为函数式编程赋予了力量。当创建一个返回值的函数时,元组将是最有用的。此外,由于在函数式编程中函数不会改变全局状态,我们可以返回元组以替代所有需要改变的值。现在,让我们来看一下下面的代码片段:
/* tuples_1.cpp */
#include <tuple>
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[tuples_1.cpp]" << endl;
// Initializing two Tuples
tuple<int, string, bool> t1(1, "Robert", true);
auto t2 = make_tuple(2, "Anna", false);
// Displaying t1 Tuple elements
cout << "t1 elements:" << endl;
cout << get<0>(t1) << endl;
cout << get<1>(t1) << endl;
cout << (get<2>(t1) == true ? "Male" : "Female") << endl;
cout << endl;
// Displaying t2 Tuple elements
cout << "t2 elements:" << endl;
cout << get<0>(t2) << endl;
cout << get<1>(t2) << endl;
cout << (get<2>(t2) == true ? "Male" : "Female") << endl;
cout << endl;
return 0;
}
在前面的代码中,我们使用tuple<int, string, bool>
和make_tuple
使用不同的构造技术创建了两个元组t1
和t2
。然而,这两种不同的技术将给出相同的结果。显然,在代码中,我们使用get<x>(y)
访问元组中的每个元素,其中x
是索引,y
是元组对象。并且,我们将在控制台上得到以下结果:
解包元组值
元组类中的另一个有用成员是 tie()
,它用于将元组解包为单独的对象或创建 lvalue
引用的元组。此外,我们在元组中还有 ignore
辅助类,用于在使用 tie()
解包元组时跳过一个元素的占位符。让我们看看下面的代码块中 tie()
和 ignore
的用法:
/* tuples_2.cpp */
#include <tuple>
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[tuples_2.cpp]" << endl;
// Initializing two Tuples
tuple<int, string, bool> t1(1, "Robert", true);
auto t2 = make_tuple(2, "Anna", false);
int i;
string s;
bool b;
// Unpacking t1 Tuples
tie(i, s, b) = t1;
cout << "tie(i, s, b) = t1" << endl;
cout << "i = " << i << endl;
cout << "s = " << s << endl;
cout << "b = " << boolalpha << b << endl;
cout << endl;
// Unpacking t2 Tuples
tie(ignore, s, ignore) = t2;
cout << "tie(ignore, s, ignore) = t2" << endl;
cout << "new i = " << i << endl;
cout << "new s = " << s << endl;
cout << "new b = " << boolalpha << b << endl;
cout << endl;
return 0;
}
在上面的代码中,我们有与 tuples_1.cpp
相同的两个元组。我们想要使用 tie()
方法将 t1
解包为变量 i
、s
和 b
。然后,我们只将 t2
解包到变量 s
中,忽略 t2
中的 int
和 bool
数据。如果我们运行代码,输出应该如下所示:
返回元组值类型
正如我们之前讨论的,当我们想要编写一个返回多个数据的函数时,我们可以在函数式编程中最大程度地利用元组。让我们看看下面的代码块,了解如何返回元组并访问返回值:
/* tuples_3.cpp */
#include <tuple>
#include <iostream>
using namespace std;
tuple<int, string, bool> GetData(int DataId)
{
if (DataId == 1)
return std::make_tuple(0, "Chloe", false);
else if (DataId == 2)
return std::make_tuple(1, "Bryan", true);
else
return std::make_tuple(2, "Zoey", false);
}
auto main() -> int
{
cout << "[tuples_3.cpp]" << endl;
auto name = GetData(1);
cout << "Details of Id 1" << endl;
cout << "ID = " << get<0>(name) << endl;
cout << "Name = " << get<1>(name) << endl;
cout << "Gender = " << (get<2>(name) == true ?
"Male" : "Female");
cout << endl << endl;
int i;
string s;
bool b;
tie(i, s, b) = GetData(2);
cout << "Details of Id 2" << endl;
cout << "ID = " << i << endl;
cout << "Name = " << s << endl;
cout << "Gender = " << (b == true ? "Male" : "Female");
cout << endl;
return 0;
}
正如我们在上面的代码中所看到的,我们有一个名为 GetData()
的新函数,返回一个 Tuple
值。从该函数中,我们将消耗返回的数据。我们首先创建名为 name 的变量,并从 GetData()
函数中获取值。我们还可以使用 tie()
方法解包从 GetData()
函数返回的元组,正如我们在代码中访问 ID = 2
时所看到的。当我们运行代码时,控制台上的输出应该如下截图所示:
总结
通过完成本章,我们已经刷新了对 C++ 语言的经验。现在我们知道 C++ 更加现代化,它带来了许多功能,帮助我们创建更好的程序。我们可以使用标准库使我们的代码更加高效,因为我们不需要编写太多冗余的函数。我们可以使用 Lambda 表达式使我们的代码整洁、易读和易于维护。我们还可以使用智能指针,这样我们就不需要再担心内存管理了。此外,由于我们关注函数式编程中的不可变性,我们将在下一章中更深入地讨论这一点;元组的使用可以帮助我们确保我们的代码中不涉及全局状态。
在下一章中,我们将讨论头等和纯函数,它用于净化我们的类并确保当前函数中不涉及外部状态。因此,它将避免我们的函数式代码中产生副作用。
第二章:在函数式编程中操作函数
在上一章中,我们深入讨论了现代 C++,特别是 C++11 中的新功能——Lambda 表达式。正如我们之前讨论的,Lambda 表达式在简化函数表示法方面非常有用。因此,在本章中,我们将再次应用 Lambda 表达式的威力,它将用于函数式代码,特别是在谈论柯里化时——这是一种分割和减少当前函数的技术。
在本章中,我们将讨论以下主题:
-
应用头等函数和高阶函数,使我们的函数不仅可以作为函数调用,还可以分配给任何变量,传递函数,并返回函数
-
纯函数,以避免我们的函数产生副作用,因为它不再接触外部状态
-
柯里化,正如本章开头提到的,以减少多个参数函数,这样我们可以评估一系列函数,每个函数中只有一个参数
在所有函数中应用头等函数
头等函数只是一个普通的类。我们可以像对待其他数据类型一样对待头等函数。然而,在支持头等函数的语言中,我们可以在不递归调用编译器的情况下执行以下任务:
-
将函数作为另一个函数的参数传递
-
将函数分配给变量
-
将函数存储在集合中
-
在运行时从现有函数创建新函数
幸运的是,C++可以用来解决前面的任务。我们将在接下来的主题中深入讨论。
将函数作为另一个函数的参数传递
让我们开始将一个函数作为函数参数传递。我们将选择四个函数中的一个,并从其主函数调用该函数。代码将如下所示:
/* first_class_1.cpp */
#include <functional>
#include <iostream>
using namespace std;
// Defining a type of function named FuncType
// representing a function
// that pass two int arguments
// and return an int value
typedef function<int(int, int)> FuncType;
int addition(int x, int y)
{
return x + y;
}
int subtraction(int x, int y)
{
return x - y;
}
int multiplication(int x, int y)
{
return x * y;
}
int division(int x, int y)
{
return x / y;
}
void PassingFunc(FuncType fn, int x, int y)
{
cout << "Result = " << fn(x, y) << endl;
}
auto main() -> int
{
cout << "[first_class_1.cpp]" << endl;
int i, a, b;
FuncType func;
// Displaying menu for user
cout << "Select mode:" << endl;
cout << "1\. Addition" << endl;
cout << "2\. Subtraction" << endl;
cout << "3\. Multiplication" << endl;
cout << "4\. Division" << endl;
cout << "Choice: ";
cin >> i;
// Preventing user to select
// unavailable modes
if(i < 1 || i > 4)
{
cout << "Please select available mode!";
return 1;
}
// Getting input from user for variable a
cout << "a -> ";
cin >> a;
// Input validation for variable a
while (cin.fail())
{
// Clearing input buffer to restore cin to a usable state
cin.clear();
// Ignoring last input
cin.ignore(INT_MAX, '\n');
cout << "You can only enter numbers.\n";
cout << "Enter a number for variable a -> ";
cin >> a;
}
// Getting input from user for variable b
cout << "b -> ";
cin >> b;
// Input validation for variable b
while (cin.fail())
{
// Clearing input buffer to restore cin to a usable state
cin.clear();
// Ignoring last input
cin.ignore(INT_MAX, '\n');
cout << "You can only enter numbers.\n";
cout << "Enter a number for variable b -> ";
cin >> b;
}
switch(i)
{
case 1: PassingFunc(addition, a, b); break;
case 2: PassingFunc(subtraction, a, b); break;
case 3: PassingFunc(multiplication, a, b); break;
case 4: PassingFunc(division, a, b); break;
}
return 0;
}
从前面的代码中,我们可以看到我们有四个函数,我们希望用户选择一个,然后运行它。在 switch 语句中,我们将根据用户的选择调用四个函数中的一个。我们将选择的函数传递给PassingFunc()
,如下面的代码片段所示:
case 1: PassingFunc(addition, a, b); break;
case 2: PassingFunc(subtraction, a, b); break;
case 3: PassingFunc(multiplication, a, b); break;
case 4: PassingFunc(division, a, b); break;
我们还有输入验证,以防止用户选择不可用的模式,以及为变量a
和b
输入非整数值。我们在屏幕上看到的输出应该是这样的:
前面的屏幕截图显示,我们从可用模式中选择了“乘法”模式。然后,我们尝试为变量a
输入r
和e
变量。幸运的是,程序拒绝了它,因为我们已经进行了输入验证。然后,我们给变量a
赋值4
,给变量b
赋值2
。正如我们期望的那样,程序给我们返回8
作为结果。
正如我们在first_class_1.cpp
程序中看到的,我们使用std::function
类和typedef
关键字来简化代码。std::function
类用于存储、复制和调用任何可调用函数、Lambda 表达式或其他函数对象,以及成员函数指针和数据成员指针。然而,typedef
关键字用作另一个类型或函数的别名。
将函数分配给变量
我们还可以将函数分配给变量,这样我们可以通过调用变量来调用函数。我们将重构first_class_1.cpp
,代码将如下所示:
/* first_class_2.cpp */
#include <functional>
#include <iostream>
using namespace std;
// Defining a type of function named FuncType
// representing a function
// that pass two int arguments
// and return an int value
typedef function<int(int, int)> FuncType;
int addition(int x, int y)
{
return x + y;
}
int subtraction(int x, int y)
{
return x - y;
}
int multiplication(int x, int y)
{
return x * y;
}
int division(int x, int y)
{
return x / y;
}
auto main() -> int
{
cout << "[first_class_2.cpp]" << endl;
int i, a, b;
FuncType func;
// Displaying menu for user
cout << "Select mode:" << endl;
cout << "1\. Addition" << endl;
cout << "2\. Subtraction" << endl;
cout << "3\. Multiplication" << endl;
cout << "4\. Division" << endl;
cout << "Choice: ";
cin >> i;
// Preventing user to select
// unavailable modes
if(i < 1 || i > 4)
{
cout << "Please select available mode!";
return 1;
}
// Getting input from user for variable a
cout << "a -> ";
cin >> a;
// Input validation for variable a
while (cin.fail())
{
// Clearing input buffer to restore cin to a usable state
cin.clear();
// Ignoring last input
cin.ignore(INT_MAX, '\n');
cout << "You can only enter numbers.\n";
cout << "Enter a number for variable a -> ";
cin >> a;
}
// Getting input from user for variable b
cout << "b -> ";
cin >> b;
// Input validation for variable b
while (cin.fail())
{
// Clearing input buffer to restore cin to a usable state
cin.clear();
// Ignoring last input
cin.ignore(INT_MAX, '\n');
cout << "You can only enter numbers.\n";
cout << "Enter a number for variable b -> ";
cin >> b;
}
switch(i)
{
case 1: func = addition; break;
case 2: func = subtraction; break;
case 3: func = multiplication; break;
case 4: func = division; break;
}
cout << "Result = " << func(a, b) << endl;
return 0;
}
我们现在将根据用户的选择分配四个函数,并将所选函数存储在func
变量中的 switch 语句内,如下所示:
case 1: func = addition; break;
case 2: func = subtraction; break;
case 3: func = multiplication; break;
case 4: func = division; break;
在func
变量被赋予用户的选择后,代码将像调用函数一样调用变量,如下面的代码行所示:
cout << "Result = " << func(a, b) << endl;
然后,如果我们运行代码,我们将在控制台上获得相同的输出。
将函数存储在容器中
现在,让我们将函数保存到容器中。在这里,我们将使用vector作为容器。代码编写如下:
/* first_class_3.cpp */
#include <vector>
#include <functional>
#include <iostream>
using namespace std;
// Defining a type of function named FuncType
// representing a function
// that pass two int arguments
// and return an int value
typedef function<int(int, int)> FuncType;
int addition(int x, int y)
{
return x + y;
}
int subtraction(int x, int y)
{
return x - y;
}
int multiplication(int x, int y)
{
return x * y;
}
int division(int x, int y)
{
return x / y;
}
auto main() -> int
{
cout << "[first_class_3.cpp]" << endl;
// Declaring a vector containing FuncType element
vector<FuncType> functions;
// Assigning several FuncType elements to the vector
functions.push_back(addition);
functions.push_back(subtraction);
functions.push_back(multiplication);
functions.push_back(division);
int i, a, b;
function<int(int, int)> func;
// Displaying menu for user
cout << "Select mode:" << endl;
cout << "1\. Addition" << endl;
cout << "2\. Subtraction" << endl;
cout << "3\. Multiplication" << endl;
cout << "4\. Division" << endl;
cout << "Choice: ";
cin >> i;
// Preventing user to select
// unavailable modes
if(i < 1 || i > 4)
{
cout << "Please select available mode!";
return 1;
}
// Getting input from user for variable a
cout << "a -> ";
cin >> a;
// Input validation for variable a
while (cin.fail())
{
// Clearing input buffer to restore cin to a usable state
cin.clear();
// Ignoring last input
cin.ignore(INT_MAX, '\n');
cout << "You can only enter numbers.\n";
cout << "Enter a number for variable a -> ";
cin >> a;
}
// Getting input from user for variable b
cout << "b -> ";
cin >> b;
// Input validation for variable b
while (cin.fail())
{
// Clearing input buffer to restore cin to a usable state
cin.clear();
// Ignoring last input
cin.ignore(INT_MAX, '\n');
cout << "You can only enter numbers.\n";
cout << "Enter a number for variable b -> ";
cin >> b;
}
// Invoking the function inside the vector
cout << "Result = " << functions.at(i - 1)(a, b) << endl;
return 0;
}
从前面的代码中,我们可以看到我们创建了一个名为 functions 的新向量,然后将四个不同的函数存储到其中。就像我们之前的两个代码示例一样,我们也要求用户选择模式。然而,现在代码变得更简单了,因为我们不需要添加 switch 语句;我们可以通过选择向量索引直接选择函数,就像我们在下面的代码片段中看到的那样:
cout << "Result = " << functions.at(i - 1)(a, b) << endl;
然而,由于向量是基于零的索引,我们必须调整菜单选择的索引。结果将与我们之前的两个代码示例相同。
在运行时从现有函数创建新函数
现在让我们从现有函数中在运行时创建一个新函数。假设我们有两个函数集合,第一个是双曲函数,第二个是第一个函数的反函数。除了这些内置函数之外,我们还在第一个集合中添加一个用户定义的函数来计算平方数,在第二个集合中添加平方数的反函数。然后,我们将实现函数组合,并从两个现有函数构建一个新函数。
函数组合是将两个或多个简单函数组合成一个更复杂的函数的过程。每个函数的结果作为下一个函数的参数传递。最终结果是从最后一个函数的结果获得的。在数学方法中,我们通常使用以下符号来表示函数组合:compose(f, g) (x) = f(g(x))
。假设我们有以下代码:
double x, y, z; // ... y = g(x); z = f(y);
因此,为了简化表示,我们可以使用函数组合,并对z有以下表示:
z = f(g(x));
如果我们运行双曲函数,然后将结果传递给反函数,我们将看到我们确实得到了传递给双曲函数的原始值。现在,让我们看一下以下代码:
/* first_class_4.cpp */
#include <vector>
#include <cmath>
#include <algorithm>
#include <functional>
#include <iostream>
using std::vector;
using std::function;
using std::transform;
using std::back_inserter;
using std::cout;
using std::endl;
// Defining a type of function named HyperbolicFunc
// representing a function
// that pass a double argument
// and return an double value
typedef function<double(double)> HyperbolicFunc;
// Initializing a vector containing four functions
vector<HyperbolicFunc> funcs = {
sinh,
cosh,
tanh,
[](double x) {
return x*x; }
};
// Initializing a vector containing four functions
vector<HyperbolicFunc> inverseFuncs = {
asinh,
acosh,
atanh,
[](double x) {
return exp(log(x)/2); }
};
// Declaring a template to be able to be reused
template <typename A, typename B, typename C>
function<C(A)> compose(
function<C(B)> f,
function<B(A)> g) {
return f,g {
return f(g(x));
};
}
auto main() -> int
{
cout << "[first_class_4.cpp]" << endl;
// Declaring a template to be able to be reused
vector<HyperbolicFunc> composedFuncs;
// Initializing a vector containing several double elements
vector<double> nums;
for (int i = 1; i <= 5; ++i)
nums.push_back(i * 0.2);
// Transforming the element inside the vector
transform(
begin(inverseFuncs),
end(inverseFuncs),
begin(funcs),
back_inserter(composedFuncs),
compose<double, double, double>);
for (auto num: nums)
{
for (auto func: composedFuncs)
cout << "f(g(" << num << ")) = " << func(num) << endl;
cout << "---------------" << endl;
}
return 0;
}
正如我们在前面的代码中所看到的,我们有两个函数集合--funcs
和inverseFuncs
。此外,正如我们之前讨论的,inverseFuncs
函数是funcs
函数的反函数。funcs
函数包含三个内置的双曲函数,以及一个用户定义的函数来计算平方数,而inverseFuncs
包含三个内置的反双曲函数,以及一个用户定义的函数来计算平方数的反函数。
正如我们在前面的first_class_4.cpp
代码中所看到的,当调用using
关键字时,我们将使用单独的类/函数。与本章中的其他代码示例相比,在单独的类/函数中使用using
关键字是不一致的,因为我们使用using namespace std
。这是因为在std
命名空间中有一个冲突的函数名称,所以我们必须单独调用它们。
通过使用这两个函数集合,我们将从中构建一个新函数。为了实现这个目的,我们将使用transform()
函数来组合来自两个不同集合的两个函数。代码片段如下:
transform(
begin(inverseFuncs),
inverseFuncs.end(inverseFuncs),
begin(funcs),
back_inserter(composedFuncs),
compose<double, double, double>);
现在,我们在composedFuncs
向量中存储了一个新的函数集合。我们可以迭代集合,并将我们在nums
变量中提供的值传递给这个新函数。如果我们运行代码,我们应该在控制台上获得以下输出:
从前面的输出中可以看出,无论我们传递给变换函数什么,我们都将得到与输入相同的输出。在这里,我们可以证明 C++编程可以用于从两个或多个现有函数组合一个函数。
在前面的first_class_4.cpp
代码中,我们在代码中使用了template<>
。如果您需要关于template<>
的更详细解释,请参考第七章,使用并行执行运行并发。
在高阶函数中熟悉三种功能技术
我们讨论了在一等函数中,C++语言将函数视为值,这意味着我们可以将它们传递给其他函数,分配给变量等。然而,在函数式编程中,我们还有另一个术语,即高阶函数,这是指可以处理其他函数的函数。这意味着高阶函数可以将函数作为参数传递,也可以返回一个函数。
高阶函数的概念可以应用于一般的函数,比如数学函数,而不仅仅是适用于函数式编程语言的一等函数概念。现在,让我们来看看函数式编程中三个最有用的高阶函数--map,filter和fold。
使用 map 执行每个元素列表
我们不会将 map 作为 C++语言中的容器来讨论,而是作为高阶函数的一个特性。这个特性用于将给定的函数应用于列表的每个元素,并按相同的顺序返回结果列表。我们可以使用transform()
函数来实现这个目的。正如你所知,我们之前已经讨论过这个函数。然而,我们可以看一下下面的代码片段,查看transform()
函数的使用:
/* transform_1.cpp */
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[transform_1.cpp]" << endl;
// Initializing a vector containing integer element
vector<int> v1;
for (int i = 0; i < 5; ++i)
v1.push_back(i);
// Creating another v2 vector
vector<int> v2;
// Resizing the size of v2 exactly same with v1
v2.resize(v1.size());
// Transforming the element inside the vector
transform (
begin(v1),
end(v1),
begin(v2),
[](int i){
return i * i;});
// Displaying the elements of v1
std::cout << "v1 contains:";
for (auto v : v1)
std::cout << " " << v;
std::cout << endl;
// Displaying the elements of v2
std::cout << "v2 contains:";
for (auto v : v2)
std::cout << " " << v;
std::cout << endl;
return 0;
}
正如我们在高阶函数中的 map 的前面定义中所看到的,它将对列表的每个元素应用给定的函数。在前面的代码中,我们尝试使用 Lambda 表达式将v1
向量映射到v2
向量,给定的函数如下:
transform (
begin(v1),
end(v1),
begin(v2),
[](int i){
return i * i;});
如果我们运行代码,应该在控制台屏幕上得到以下输出:
正如我们在输出显示中所看到的,我们使用 Lambda 表达式中给定的函数将v1
转换为v2
,这个函数是将输入值加倍。
使用过滤器提取数据
在高阶函数中,过滤器是一个从现有数据结构中生成新数据结构的函数,新数据结构中的每个元素都与返回布尔值的给定谓词完全匹配。在 C++语言中,我们可以应用copy_if()
函数,它是在 C++11 中添加的,来进行过滤过程。让我们看一下下面的代码片段,分析使用copy_if()
函数进行过滤过程:
/* filter_1.cpp */
#include <vector>
#include <algorithm>
#include <iterator>
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[filter_1.cpp]" << endl;
// Initializing a vector containing integer elements
vector<int> numbers;
for (int i = 0; i < 20; ++i)
numbers.push_back(i);
// Displaying the elements of numbers
cout << "The original numbers: " << endl;
copy(
begin(numbers),
end(numbers),
ostream_iterator<int>(cout, " "));
cout << endl;
// Declaring a vector containing int elements
vector<int> primes;
// Filtering the vector
copy_if(
begin(numbers),
end(numbers),
back_inserter(primes),
[](int n) {
if(n < 2) {
return (n != 0) ? true : false;}
else {
for (int j = 2; j < n; ++j) {
if (n % j == 0){
return false;}
}
return true;
}});
// Displaying the elements of primes
// using copy() function
cout << "The primes numbers: " << endl;
copy(
begin(primes),
end(primes),
ostream_iterator<int>(cout, " "));
cout << endl;
return 0;
}
正如我们在前面的代码中所看到的,我们使用copy_if()
函数将numbers
向量过滤为0
素数向量。我们将传递 Lambda 表达式来决定所选元素是否为素数,就像我们在第一章中的lambda_multiline_func.cpp
代码中所使用的那样,我们还将使用copy()
函数将所选向量中的所有元素复制出来打印。当我们运行前面的代码时,结果应该是这样的:
除了copy_if()
函数,我们还可以使用remove_copy_if()
函数来过滤数据结构。使用remove_copy_if()
函数将不选择匹配谓词元素的现有数据结构中的元素,而是选择不匹配的元素,并将其存储在新的数据结构中。让我们重构我们的filter_1.cpp
代码,并创建一个不是素数的新向量。代码将如下所示:
/* filter_2.cpp */
#include <vector>
#include <algorithm>
#include <iterator>
#include <iostream>
using namespace std;
int main()
{
cout << "[filter_2.cpp]" << endl;
// Initializing a vector containing integer elements
vector<int> numbers;
for (int i = 0; i < 20; ++i)
numbers.push_back(i);
// Displaying the elements of numbers
cout << "The original numbers: " << endl;
copy(
begin(numbers),
end(numbers),
ostream_iterator<int>(cout, " "));
cout << endl;
// Declaring a vector containing int elements
vector<int> nonPrimes;
// Filtering the vector
remove_copy_if(
numbers.begin(),
numbers.end(),
back_inserter(nonPrimes),
[](int n) {
if(n < 2){
return (n != 0) ? true : false;}
else {
for (int j = 2; j < n; ++j){
if (n % j == 0) {
return false;}
}
return true;
}});
// Displaying the elements of nonPrimes
// using copy() function
cout << "The non-primes numbers: " << endl;
copy(
begin(nonPrimes),
end(nonPrimes),
ostream_iterator<int>(cout, " "));
cout << endl;
return 0;
}
从前面突出显示的代码中,我们重构了以前的代码,并使用remove_copy_if()
函数选择非素数。正如我们所期望的,控制台窗口将显示以下输出:
现在我们有了非素数,而不是像在filter_1.cpp
代码中那样的素数。
使用折叠组合列表的所有元素
在函数式编程中,折叠是一种将数据结构减少为单个值的技术。有两种类型的折叠——左折叠(foldl
)和右折叠(foldr
)。假设我们有一个包含 0、1、2、3 和 4 的列表。让我们使用折叠技术来添加列表的所有内容,首先使用foldl
,然后使用foldr
。然而,两者之间有一个显著的区别——foldl
是左结合的,这意味着我们首先组合最左边的元素,然后向右边移动。例如,通过我们拥有的列表,我们将得到以下括号:
((((0 + 1) + 2) + 3) + 4)
而foldr
是右结合的,这意味着我们将首先组合最右边的元素,然后向左边移动。括号将如下代码行所示:
(0 + (1 + (2 + (3 + 4))))
现在,让我们来看一下以下的代码:
/* fold_1.cpp */
#include <vector>
#include <numeric>
#include <functional>
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[fold_1.cpp]" << endl;
// Initializing a vector containing integer elements
vector<int> numbers = {0, 1, 2, 3, 4};
// Calculating the sum of the value
// in the vector
auto foldl = accumulate(
begin(numbers),
end(numbers),
0,
std::plus<int>());
// Calculating the sum of the value
// in the vector
auto foldr = accumulate(
rbegin(numbers),
rend(numbers),
0,
std::plus<int>());
// Displaying the calculating result
cout << "foldl result = " << foldl << endl;
cout << "foldr result = " << foldr << endl;
return 0;
}
在 C++编程中,我们可以使用accumulate()
函数应用fold
技术。正如我们在前面的代码中看到的,我们在foldl
中使用前向迭代器,而在foldr
中使用后向迭代器。控制台上的输出应该如下截图所示:
正如我们在前面的输出截图中看到的,我们对foldl
和foldr
技术得到了相同的结果。对于那些好奇求和的顺序的人,我们可以将前面的代码重构为以下代码:
/* fold_2.cpp */
#include <vector>
#include <numeric>
#include <functional>
#include <iostream>
using namespace std;
// Function for logging the flow
int addition(const int& x, const int& y)
{
cout << x << " + " << y << endl;
return x + y;
}
int main()
{
cout << "[fold_2.cpp]" << endl;
// Initializing a vector containing integer elements
vector<int> numbers = {0, 1, 2, 3, 4};
// Calculating the sum of the value
// in the vector
// from left to right
cout << "foldl" << endl;
auto foldl = accumulate(
begin(numbers),
end(numbers),
0,
addition);
// Calculating the sum of the value
// in the vector
// from right to left
cout << endl << "foldr" << endl;
auto foldr = accumulate(
rbegin(numbers),
rend(numbers),
0,
addition);
cout << endl;
// Displaying the calculating result
cout << "foldl result = " << foldl << endl;
cout << "foldr result = " << foldr << endl;
return 0;
}
在前面的代码中,我们传递了一个新的addition()
函数并将其传递给accumulate()
函数。从addition()
函数中,我们将跟踪每个元素的操作。现在,让我们运行前面的代码,其输出将如下所示:
从前面的输出截图中,我们可以看到,即使foldl
和foldr
都给出了完全相同的结果,它们执行的操作顺序不同。由于我们将初始值设置为0
,加法操作从foldl
技术中的第一个元素开始,然后在foldr
技术中的最后一个元素开始。
我们将初始值设置为0
,因为0
是不会影响加法结果的加法恒等元。然而,在乘法中,我们必须考虑将初始值更改为1
,因为1
是乘法的单位元。
通过纯函数避免副作用
纯函数是一个函数,每次给定相同的输入时都会返回相同的结果。结果不依赖于任何信息或状态,也不会产生副作用,或者改变函数之外的系统状态。让我们看看以下代码片段:
/* pure_function_1.cpp */
#include <iostream>
using namespace std;
float circleArea(float r)
{
return 3.14 * r * r;
}
auto main() -> int
{
cout << "[pure_function_1.cpp]" << endl;
// Initializing a float variable
float f = 2.5f;
// Invoking the circleArea() function
// passing the f variable five times
for(int i = 1; i <= 5; ++i)
{
cout << "Invocation " << i << " -> ";
cout << "Result of circleArea(" << f << ") = ";
cout << circleArea(f) << endl;
}
return 0;
}
从前面的代码中,我们可以看到一个名为circleArea()
的函数,根据给定的半径计算圆的面积。然后我们调用该函数五次并传递相同的半径值。控制台上的输出应该如下截图所示:
正如我们所看到的,在五次调用中传递相同的输入,函数返回的输出也是相同的。因此我们可以说circleArea()
是一个纯函数。现在,让我们看看在以下代码片段中不纯的函数是什么样子的:
/* impure_function_1.cpp */
#include <iostream>
using namespace std;
// Initializing a global variable
int currentState = 0;
int increment(int i)
{
currentState += i;
return currentState;
}
auto main() -> int
{
cout << "[impure_function_1.cpp]" << endl;
// Initializing a local variable
int fix = 5;
// Involving the global variable
// in the calculation
for(int i = 1; i <= 5; ++i)
{
cout << "Invocation " << i << " -> ";
cout << "Result of increment(" << fix << ") = ";
cout << increment(fix) << endl;
}
return 0;
}
在前面的代码中,我们看到一个名为increment()
的函数增加了currentState
变量的值。正如我们所看到的,increment()
函数依赖于currentState
变量的值,因此它不是一个纯函数。让我们通过运行前面的代码来证明这一点。控制台窗口应该显示以下截图:
我们看到increment()
函数给出了不同的结果,即使我们传递相同的输入。这是不纯函数的副作用,当它依赖于外部状态或改变外部状态的值时。
我们已经能够区分纯函数和不纯函数。然而,请考虑以下代码:
/* im_pure_function_1.cpp */
#include <iostream>
using namespace std;
// Initializing a global variable
float phi = 3.14f;
float circleArea(float r)
{
return phi * r * r;
}
auto main() -> int
{
cout << "[im_pure_function_1.cpp]" << endl;
// Initializing a float variable
float f = 2.5f;
// Involving the global variable
// in the calculation
for(int i = 1; i <= 5; ++i)
{
cout << "Invocation " << i << " -> ";
cout << "Result of circleArea(" << f << ") = ";
cout << circleArea(f) << endl;
}
return 0;
}
上述代码来自pure_function_1.cpp
,但我们添加了一个全局状态phi
。如果我们运行上述代码,我们肯定会得到与pure_function_1.cpp
相同的结果。尽管函数在五次调用中返回相同的结果,但im_pure_function_1.cpp
中的circleArea()
不是一个纯函数,因为它依赖于phi
变量。
副作用不仅是函数所做的全局状态的改变。向屏幕打印也是副作用。然而,由于我们需要显示我们创建的每个代码的结果,我们无法避免在我们的代码中存在向屏幕打印的情况。在下一章中,我们还将讨论不可变状态,这是我们可以将不纯函数转变为纯函数的方法。
使用柯里化来减少多参数函数
柯里化是一种将接受多个参数的函数拆分为评估一系列具有单个参数的函数的技术。换句话说,我们通过减少当前函数来创建其他函数。假设我们有一个名为areaOfRectangle()
的函数,它接受两个参数,length
和width
。代码将如下所示:
/* curry_1.cpp */
#include <functional>
#include <iostream>
using namespace std;
// Variadic template for currying
template<typename Func, typename... Args>
auto curry(Func func, Args... args)
{
return =
{
return func(args..., lastParam...);
};
}
int areaOfRectangle(int length, int width)
{
return length * width;
}
auto main() -> int
{
cout << "[curry_1.cpp]" << endl;
// Currying the areaOfRectangle() function
auto length5 = curry(areaOfRectangle, 5);
// Invoking the curried function
cout << "Curried with spesific length = 5" << endl;
for(int i = 0; i <= 5; ++i)
{
cout << "length5(" << i << ") = ";
cout << length5(i) << endl;
}
return 0;
}
如前面的代码中所示,我们有一个可变模板和名为curry
的函数。我们将使用此模板来构建一个柯里化函数。在正常的函数调用中,我们可以如下调用areaOfRectangle()
函数:
int i = areaOfRectangle(5, 2);
如前面的代码片段中所示,我们将5
和2
作为参数传递给areaOfRectangle()
函数。但是,使用柯里化函数,我们可以减少areaOfRectangle()
函数,使其只有一个参数。我们只需要调用柯里化函数模板,如下所示:
auto length5 = curry(areaOfRectangle, 5);
现在,我们有了areaOfRectangle()
函数,它具有名为length5
的length
参数的值。我们可以更容易地调用函数,并只添加width
参数,如下面的代码片段所示:
length5(i) // where i is the width parameter we want to pass
让我们来看看当我们运行上述代码时,控制台上会看到的输出:
可变模板和函数已经帮助我们将areaOfRectangle()
函数减少为length5()
函数。但是,它也可以帮助我们减少具有两个以上参数的函数。假设我们有一个名为volumeOfRectanglular()
的函数,它传递三个参数。我们也将减少该函数,如下面的代码所示:
/* curry_2.cpp */
#include <functional>
#include <iostream>
using namespace std;
// Variadic template for currying
template<typename Func, typename... Args>
auto curry(Func func, Args... args)
{
return =
{
return func(args..., lastParam...);
};
}
int volumeOfRectanglular(
int length,
int width,
int height)
{
return length * width * height;
}
auto main() -> int
{
cout << "[curry_2.cpp]" << endl;
// Currying the volumeOfRectanglular() function
auto length5width4 = curry(volumeOfRectanglular, 5, 4);
// Invoking the curried function
cout << "Curried with spesific data:" << endl;
cout << "length = 5, width 4" << endl;
for(int i = 0; i <= 5; ++i)
{
cout << "length5width4(" << i << ") = ";
cout << length5width4(i) << endl;
}
return 0;
}
如前面的代码中所示,我们已成功将length
和width
参数传递给volumeOfRectanglular()
函数,然后将其减少为length5width4()
。我们可以调用length5width4()
函数,并只传递其余参数height
。如果我们运行上述代码,我们将在控制台屏幕上看到以下输出:
通过使用柯里化技术,我们可以通过减少函数来部分评估多个参数函数,使其只传递单个参数。
总结
我们已经讨论了一些操纵函数的技术。我们将从中获得许多优势。由于我们可以在 C++语言中实现头等函数,我们可以将一个函数作为另一个函数的参数传递。我们可以将函数视为数据对象,因此可以将其分配给变量并存储在容器中。此外,我们可以从现有函数中组合出一个新的函数。此外,通过使用 map、filter 和 fold,我们可以在我们创建的每个函数中实现高阶函数。
我们还有另一种技术可以实现更好的函数式代码,那就是使用纯函数来避免副作用。我们可以重构所有函数,使其不会与外部变量或状态交互,并且不会改变或检索外部状态的值。此外,为了减少多参数函数,以便我们可以评估其顺序,我们可以将柯里化技术应用到我们的函数中。
在下一章中,我们将讨论另一种避免副作用的技术。我们将使代码中的所有状态都是不可变的,这样每次调用函数时都不会发生状态变化。
第三章:将不可变状态应用于函数
在上一章讨论了头等函数和纯函数之后,现在让我们谈谈可变和不可变对象。正如您所学到的,我们必须能够在头等函数中将一个函数传递给另一个函数,并确保如果我们传递相同的参数,函数返回相同的值。我们将讨论的不可变对象可以帮助我们使这两个函数式编程概念在我们的代码中可用。本章我们将讨论以下主题:
-
以函数式编程方法修改变量
-
演示使用
const
关键字来避免值修改 -
将头等函数和纯函数应用于不可变对象
-
将可变对象重构为不可变对象
-
不可变对象比可变对象的好处
从不可变对象中理解基本部分
在面向对象编程中,我们通常多次操纵变量对象,甚至在类本身内部,我们通常描述为属性。此外,我们有时会从特定函数更改全局变量。然而,为了在函数式编程中获得不可变性特性,我们必须遵守两条规则。首先,我们不允许更改局部变量。其次,我们必须避免在函数中涉及全局变量,因为这将影响函数结果。
修改局部变量
当我们谈论变量时,我们谈论的是一个容器,用于存储我们的数据。在我们日常编程中,我们通常会重用我们创建的变量。为了更清楚,让我们看一下mutable_1.cpp
代码。我们有mutableVar
变量并将100
存储到其中。然后我们为i
变量迭代操纵其值。代码如下所示:
/* mutable_1.cpp */
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[mutable_1.cpp]" << endl;
// Initializing an int variable
int mutableVar = 100;
cout << "Initial mutableVar = " << mutableVar;
cout << endl;
// Manipulating mutableVar
for(int i = 0; i <= 10; ++i)
mutableVar = mutableVar + i;
// Displaying mutableVar value
cout << "After manipulating mutableVar = " << mutableVar;
cout << endl;
return 0;
}
我们在屏幕上应该看到的结果将如下截图所示:
正如我们所看到的,我们成功地操纵了mutableVar
变量。然而,我们将mutableVar
变量视为可变对象。这是因为我们多次重用mutableVar
变量。换句话说,我们打破了之前讨论的不可变规则。如果我们愿意,我们可以重构mutable_1.cpp
代码成为不可变的。让我们分析immutable_1.cpp
代码。在这里,每次我们打算改变之前的变量时,我们将创建一个新的局部变量。代码如下所示:
/* immutable_1.cpp */
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[immutable_1.cpp]" << endl;
// Initializing an int variable
int mutableVar = 100;
cout << "Initial mutableVar = " << mutableVar;
cout << endl;
// Manipulating mutableVar using immutable approach
int mutableVar0 = mutableVar + 0;
int mutableVar1 = mutableVar0 + 1;
int mutableVar2 = mutableVar1 + 2;
int mutableVar3 = mutableVar2 + 3;
int mutableVar4 = mutableVar3 + 4;
int mutableVar5 = mutableVar4 + 5;
int mutableVar6 = mutableVar5 + 6;
int mutableVar7 = mutableVar6 + 7;
int mutableVar8 = mutableVar7 + 8;
int mutableVar9 = mutableVar8 + 9;
int mutableVar10 = mutableVar9 + 10;
// Displaying mutableVar value in mutable variable
cout << "After manipulating mutableVar = " << mutableVar10;
cout << endl;
return 0;
}
正如我们所看到的,为了避免更改局部变量mutableVar
,我们创建了其他十个局部变量。结果存储在mutableVar10
变量中。然后我们将结果显示到控制台。的确,在我们的编程活动习惯中,这是不常见的。然而,这是我们可以做到获得不可变对象的方式。通过采用这种不可变方法,我们永远不会错过以前的状态,因为我们拥有所有状态。此外,通过运行immutable_1.cpp
获得的输出与mutable_1.cpp
代码的输出完全相同,如我们在以下截图中所见:
然而,由于immutable_1.cpp
代码中有更多的代码行比mutable_1.cpp
代码,因此immutable_1.cpp
代码的性能将比mutable_1.cpp
代码慢。此外,当然,mutable_1.cpp
代码比immutable_1.cpp
代码更有效率。
修改传递给函数的变量
现在,我们将讨论当变量传递给函数时如何修改变量。假设我们有一个名为n
的变量,其中包含一个字符串数据。然后,我们将其作为参数传递给名为Modify()
的函数。在函数内部,我们操纵了名称变量。让我们看一下以下immutable_2.cpp
代码并分析它:
/* immutable_2.cpp */
#include <iostream>
using namespace std;
void Modify(string name)
{
name = "Alexis Andrews";
}
auto main() -> int
{
cout << "[immutable_2.cpp]" << endl;
// Initializing a string variable
string n = "Frankie Kaur";
cout << "Initial name = " << n;
cout << endl;
// Invoking Modify() function
// to modify the n variable
Modify(n);
// Displaying n value
cout << "After manipulating = " << n;
cout << endl;
return 0;
}
从前面的代码中,我们看到将Frankie Kaur
存储为n
变量的初始值,然后在Modify()
函数中修改为Alexis Andrews
。现在,让我们看看运行前面的代码时屏幕上的输出:
从前面的截图中可以看出,尽管我们在Modify()
函数中对其进行了修改,但name
变量仍然包含Frankie Kaur
作为其值。这是因为我们在main()
函数中传递了n
变量,而Modify()
函数接收了存储在name
变量中的值的副本,因此name
变量保持不变,包含原始值。如果我们将其作为引用传递,我们可以改变n
变量,就像我们在下面的mutable_2.cpp
代码中看到的那样:
/* mutable_2.cpp */
#include <iostream>
using namespace std;
void Modify(string &name)
{
name = "Alexis Andrews";
}
auto main() -> int
{
cout << "[mutable_2.cpp]" << endl;
// Initializing a string variable
string n = "Frankie Kaur";
cout << "Initial name = " << n;
cout << endl;
// Invoking Modify() function
// to modify the n variable
Modify(n);
// Displaying n value
cout << "After manipulating = " << n;
cout << endl;
return 0;
}
只需在Modify()
函数的参数中添加&
符号,现在将参数作为引用传递。屏幕上的输出将如下截图所示:
根据前面的截图,n
变量现在已经成功在Modify()
函数中被更改,因为我们传递的是n
变量的引用,而不是值本身。还有另一种更好的方法来改变变量,使用结构体或类类型,就像我们在下面的mutable_2a.cpp
代码中看到的那样:
/* mutable_2a.cpp */
#include <iostream>
using namespace std;
class Name
{
public:
string str;
};
void Modify(Name &name)
{
name.str = "Alexis Andrews";
}
auto main() -> int
{
cout << "[mutable_2a.cpp]" << endl;
// Initializing a string variable
Name n = {"Frankie Kaur"};
cout << "Initial name = " << n.str;
cout << endl;
// Invoking Modify() function
// to modify the n variable
Modify(n);
// Displaying n value
cout << "After manipulating = " << n.str;
cout << endl;
return 0;
}
从前面的代码中,我们可以看到一个名为Name
的类,其中包含一个字符串变量。一开始,我们使用初始值实例化Name
类。然后我们修改了类内部的str
值。如果我们运行代码,我们将得到与mutable_2.cpp
代码完全相同的输出。然而,我们看到尽管n
变量没有改变,name.str
却改变了。
防止值的修改
不可变性的关键点是防止值的修改。在 C++编程语言中,有一个关键字可以防止代码修改值。这个关键字是const
,我们将在const.cpp
代码中使用它。我们有一个名为MyAge
的类,其中包含一个名为age
的公共字段,我们将其设置为const
。我们将对这个const
字段进行操作,代码将如下所示:
/* const.cpp */
#include <iostream>
using namespace std;
// My Age class will store an age value
class MyAge
{
public:
const int age;
MyAge(const int initAge = 20) :
age(initAge)
{
}
};
auto main() -> int
{
cout << "[const.cpp]" << endl;
// Initializing several MyAge variables
MyAge AgeNow, AgeLater(8);
// Displaying age property in AgeNow instance
cout << "My current age is ";
cout << AgeNow.age << endl;
// Displaying age property in AgeLater instance
cout << "My age in eight years later is ";
cout << AgeLater.age << endl;
return 0;
}
在前面的代码中,我们实例化了两个MyAge
类;它们分别是AgeNow
和AgeLater
。对于AgeNow
,我们使用年龄的初始值,而对于AgeLater
,我们将8
赋给age
字段。控制台上的输出将如下所示:
然而,不可能插入对年龄字段的赋值。以下的const_error.cpp
代码将无法运行,因为编译器会拒绝它:
/* const_error.cpp */
#include <iostream>
using namespace std;
// My Age class will store an age value
class MyAge
{
public:
const int age;
MyAge(const int initAge = 20) :
age(initAge)
{
}
};
auto main() -> int
{
cout << "[const_error.cpp]" << endl;
// Initializing several MyAge variables
MyAge AgeNow, AgeLater(8);
// Displaying age property in AgeNow instance
cout << "My current age is ";
cout << AgeNow.age << endl;
// Displaying age property in AgeLater instance
cout << "My age in eight years later is ";
cout << AgeLater.age << endl;
// Trying to assign age property
// in AgeLater instance
// However, the compiler will refuse it
AgeLater.age = 10;
return 0;
}
正如我们所看到的,我们将age
的值修改为10
。编译器将拒绝运行,因为age
被设置为const
,并显示以下错误:
因此,我们成功地通过添加const
关键字创建了一个不可变对象。
将头等函数和纯函数应用于不可变对象
从前面的讨论中,我们对不可变对象有了一个介绍。正如您在上一章中所学到的,我们可以利用头等函数和纯函数来创建一种不可变的编程方法。让我们借用第二章中的代码,在函数式编程中操作函数,即first_class_1.cpp
。我们将在下面的first_class_pure_immutable.cpp
代码中拥有addition()
、subtraction()
、multiplication()
和division()
方法。然后我们将在类上调用纯函数,并将结果赋给变量。代码如下所示:
/* first_class_pure_immutable.cpp */
#include <iostream>
using namespace std;
// MyValue class stores the value
class MyValue
{
public:
const int value;
MyValue(int v) : value(v)
{
}
};
// MyFunction class stores the methods
class MyFunction
{
public:
const int x, y;
MyFunction(int _x, int _y) :
x(_x), y(_y)
{
}
MyValue addition() const
{
return MyValue(x + y);
}
MyValue subtraction() const
{
return MyValue(x - y);
}
MyValue multiplication() const
{
return MyValue(x * y);
}
MyValue division() const
{
return MyValue(x / y);
}
};
auto main() -> int
{
cout << "[first_class_pure_immutable.cpp]" << endl;
// Setting the initial value
// for MyFunction class constructor
int a = 100;
int b = 10;
// Displaying initial value
cout << "Initial value" << endl;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << endl;
// Constructing the MyFunction class
MyFunction func(a, b);
// Generating wrapper for each function
// in the MyFunction class
// so it will be the first-class function
auto callableAdd = mem_fn(&MyFunction::addition);
auto callableSub = mem_fn(&MyFunction::subtraction);
auto callableMul = mem_fn(&MyFunction::multiplication);
auto callableDiv = mem_fn(&MyFunction::division);
// Invoking the functions
auto value1 = callableAdd(func);
auto value2 = callableSub(func);
auto value3 = callableMul(func);
auto value4 = callableDiv(func);
// Displaying result
cout << "The result" << endl;
cout << "addition = " << value1.value << endl;
cout << "subtraction = " << value2.value << endl;
cout << "multiplication = " << value3.value << endl;
cout << "division = " << value4.value << endl;
return 0;
}
正如我们在前面的代码中所看到的,addition()
、subtraction()
、multiplication()
和division()
方法是纯函数,因为只要它们接收相同的输入,它们就会产生相同的输出。我们还创建了一个名为MyValue
的类,并将其设置为const
以使其不可变。然后,为了使我们的函数成为一流函数,我们使用mem_fn()
函数将每个方法包装在MyFunction
类中。然后,我们使用函数包装器分配了四个变量。屏幕上的输出应该如下截图所示:
开发不可变对象
在我们讨论了不可变性的概念之后,现在让我们开发不可变对象。我们将从可变对象开始,然后将其重构为不可变对象。
从可变对象开始
现在,让我们继续。我们将创建另一个类来设计一个不可变对象。首先,我们将创建一个名为MutableEmployee
的可变类。在该类中有一些字段和方法。该类的头文件将如下所示:
/* mutableemployee.h */
#ifndef __MUTABLEEMPLOYEE_H__
#define __MUTABLEEMPLOYEE_H__
#include <string>
class MutableEmployee
{
private:
int m_id;
std::string m_firstName;
std::string m_lastName;
double m_salary;
public:
MutableEmployee(
int id,
const std::string& firstName,
const std::string& lastName,
const double& salary);
MutableEmployee();
void SetId(const int id);
void SetFirstName(
const std::string& FirstName);
void SetLastName(
const std::string& LastName);
void SetSalary(
const double& Salary);
int Id() const {return m_id;}
std::string FirstName() const {return m_firstName;}
std::string LastName() const {return m_lastName;}
double Salary() const {return m_salary;}
};
#endif // End of __MUTABLEEMPLOYEE_H__
正如我们所看到的,我们有四个字段--m_id
、m_firstName
、m_lastName
和m_salary
。我们还定义了四个方法来存储这些字段的任何值。这些方法的实现如下:
/* mutableemployee.cpp */
#include "mutableemployee.h"
using namespace std;
MutableEmployee::MutableEmployee() :
m_id(0),
m_salary(0.0)
{
}
MutableEmployee::MutableEmployee(
int id,
const string& firstName,
const string& lastName,
const double& salary) :
m_id(id),
m_firstName(firstName),
m_lastName(lastName),
m_salary(salary)
{
}
void MutableEmployee::SetId(const int id)
{
m_id = id;
}
void MutableEmployee::SetFirstName(
const std::string& FirstName) {
m_firstName = FirstName;
}
void MutableEmployee::SetLastName(
const std::string& LastName) {
m_lastName = LastName;
}
void MutableEmployee::SetSalary(
const double& Salary) {
m_salary = Salary;
}
正如我们在前面的代码中所看到的,我们有一个良好的面向对象的代码,其中成员是私有的;然而,我们可以通过 setter 和 getter 访问它们。换句话说,任何代码都可以更改任何值,因此它是可变的。现在,让我们使用即将到来的mutable_3.cpp
代码来使用前面的类。我们将使用初始值实例化该类,并尝试改变它们。代码将如下所示:
/* mutable_3.cpp */
#include <iostream>
#include "../mutableemployee/mutableemployee.h"
using namespace std;
auto main() -> int
{
cout << "[mutable_3.cpp]" << endl;
// Initializing several variables
string first = "Frankie";
string last = "Kaur";
double d = 1500.0;
// Creating an instance of MutableEmployee
MutableEmployee me(0, first, last, d);
// Displaying initial value
cout << "Content of MutableEmployee instance" << endl;
cout << "ID : " << me.Id() << endl;
cout << "Name : " << me.FirstName();
cout << " " << me.LastName() << endl;
cout << "Salary : " << me.Salary() << endl << endl;
// Mutating the instance of MutableEmployee
me.SetId(1);
me.SetFirstName("Alexis");
me.SetLastName("Andrews");
me.SetSalary(2100.0);
// Displaying mutate value
cout << "Content of MutableEmployee after mutating" << endl;
cout << "ID : " << me.Id() << endl;
cout << "Name : " << me.FirstName();
cout << " " << me.LastName() << endl;
cout << "Salary : " << me.Salary() << endl;
return 0;
}
正如我们在前面的代码中所看到的,我们将初始值存储在三个变量--first
、last
和d
中。然后我们将成功地使用 setter 改变实例。输出应该如下所示:
前面的截图显示了MutableEmployee
类的变异结果。由于我们需要避免通过避免变异状态来避免副作用,我们必须将类重构为不可变类。
将可变对象重构为不可变对象
正如我们之前讨论的,为了避免副作用,我们必须设计我们的类为不可变对象。我们将重构以前的MutableEmployee
类。让我们看一下以下头文件类:
/* immutableemployee.h */
#ifndef __IMMUTABLEEMPLOYEE_H__
#define __IMMUTABLEEMPLOYEE_H__
#include <string>
class ImmutableEmployee
{
private:
int m_id;
std::string m_firstName;
std::string m_lastName;
double m_salary;
public:
ImmutableEmployee(
const int id,
const std::string& firstName,
const std::string& lastName,
const double& _salary);
ImmutableEmployee();
const int Id() const {
return m_id;
}
const std::string& FirstName() const {
return m_firstName;
}
const std::string& LastName() const {
return m_lastName;
}
const double Salary() const {
return m_salary;
}
};
#endif // End of __IMMUTABLEEMPLOYEE_H__
正如我们在前面的头文件代码中所看到的,我们从以前的MutableEmployee
类中删除了 setter。我们这样做是为了使ImmutableEmployee
类成为不可变的。头文件的实现可以在以下代码中找到:
/* immutableemployee.cpp */
#include "immutableemployee.h"
using namespace std;
ImmutableEmployee::ImmutableEmployee() :
m_id(0),
m_salary(0.0)
{
}
ImmutableEmployee::ImmutableEmployee(
const int id,
const string& firstName,
const string& lastName,
const double& salary) :
m_id(id),
m_firstName(firstName),
m_lastName(lastName),
m_salary(salary)
{
}
现在,让我们分析ImmutableEmployee
类并将其与MutableEmployee
类进行比较。我们应该得到以下结果:
-
我们现在将所有成员变量设置为
const
,这意味着变量只能在构造函数中初始化。这将是创建不可变对象的最佳方法。然而,const
成员阻止将移动操作应用于其他成员,这是一个巧妙的 C++11 优化。 -
获取方法现在返回
const
引用而不是值。由于不可变对象不能修改值,最好返回对它们的引用。 -
获取器现在返回
const
值,以避免结果被其他语句修改。它还可以防止一些常见错误,比如在比较中使用=
而不是==
。它声明了我们使用不可变类型的事实。
如果我们想要更改m_firstName
或m_salary
字段,就会出现问题。为了解决这个问题,我们可以向ImmutableEmployee
类添加 setter。然而,它现在返回ImmutableEmployee
实例,而不是变异字段目标。immutableemployee.h
代码将如下所示:
/* immutableemployee.h */
#ifndef __IMMUTABLEEMPLOYEE_H__
#define __IMMUTABLEEMPLOYEE_H__
#include <string>
class ImmutableEmployee
{
private:
int m_id;
std::string m_firstName;
std::string m_lastName;
double m_salary;
public:
ImmutableEmployee(
const int id,
const std::string& firstName,
const std::string& lastName,
const double& _salary);
ImmutableEmployee();
~ImmutableEmployee();
const int Id() const {
return m_id;
}
const std::string& FirstName() const {
return m_firstName;
}
const std::string& LastName() const {
return m_lastName;
}
const double Salary() const {
return m_salary;
}
const ImmutableEmployee SetId(
const int id) const {
return ImmutableEmployee(
id, m_firstName, m_lastName, m_salary);
}
const ImmutableEmployee SetFirstName(
const std::string& firstName) const {
return ImmutableEmployee(
m_id, firstName, m_lastName, m_salary);
}
const ImmutableEmployee SetLastName(
const std::string& lastName) const {
return ImmutableEmployee(
m_id, m_firstName, lastName, m_salary);
}
const ImmutableEmployee SetSalary(
const double& salary) const {
return ImmutableEmployee(
m_id, m_firstName, m_lastName, salary);
}
};
#endif // End of __IMMUTABLEEMPLOYEE_H__
正如我们现在所看到的,在immutableemployee.h
文件中,我们有四个 setter。它们是SetId
、SetFirstName
、SetLastName
和SetSalary
。尽管ImmutableEmployee
类中 setter 的名称与MutableEmployee
类完全相同,但在ImmutableEmployee
类中,setter 会返回类的实例,正如我们之前讨论的那样。通过使用这个ImmutableEmployee
类,我们必须采用函数式方法,因为这个类是不可变对象。以下的代码是immutable_3.cpp
,我们从mutable_3.cpp
文件中重构而来:
/* immutable_3.cpp */
#include <iostream>
#include "../immutableemployee/immutableemployee.h"
using namespace std;
auto main() -> int
{
cout << "[immutable_3.cpp]" << endl;
// Initializing several variables
string first = "Frankie";
string last = "Kaur";
double d = 1500.0;
// Creating the instance of ImmutableEmployee
ImmutableEmployee me(0, first, last, d);
// Displaying initial value
cout << "Content of ImmutableEmployee instance" << endl;
cout << "ID : " << me.Id() << endl;
cout << "Name : " << me.FirstName()
<< " " << me.LastName() << endl;
cout << "Salary : " << me.Salary() << endl << endl;
// Modifying the initial value
ImmutableEmployee me2 = me.SetId(1);
ImmutableEmployee me3 = me2.SetFirstName("Alexis");
ImmutableEmployee me4 = me3.SetLastName("Andrews");
ImmutableEmployee me5 = me4.SetSalary(2100.0);
// Displaying the new value
cout << "Content of ImmutableEmployee after modifying" << endl;
cout << "ID : " << me5.Id() << endl;
cout << "Name : " << me5.FirstName()
<< " " << me5.LastName() << endl;
cout << "Salary : " << me5.Salary() << endl;
return 0;
}
正如我们在前面的代码中看到的,我们通过实例化其他四个ImmutableEmployee
类--me2
、me3
、me4
和me5
--来修改内容。这类似于我们在immutable_1.cpp
中所做的。然而,我们现在处理的是一个类。前面代码的输出应该看起来像以下的截图:
通过获得前面的输出,我们可以说我们已经成功地修改了ImmutableEmployee
类的实例,而不是对其进行突变。
列举不可变性的好处
经过我们的讨论,我们现在知道不可变对象是函数式编程的重要部分。以下是我们可以从不可变对象中获得的好处:
-
我们不会处理副作用。这是因为我们已经确保没有外部状态被修改。我们每次打算改变对象内部的值时,也会创建一个新对象。
-
没有无效对象的状态。这是因为我们总是处于一个不一致的状态。如果我们忘记调用特定的方法,我们肯定会得到正确的状态,因为方法之间没有连接。
-
它将是线程安全的,因为我们可以同时运行许多方法,而无需锁定在池中运行的第一个方法。换句话说,我们永远不会遇到任何同步问题。
摘要
首先,在本章中,我们尝试以函数式的方式修改局部变量。我们无法重用我们创建的变量;相反,当我们需要修改它时,我们必须创建另一个变量。我们还讨论了将变量传递给另一个函数进行修改的技术。我们必须通过引用传递参数,而不是按值传递参数,以使其改变。
然后,我们深入使用const
关键字来为函数提供不可变行为。通过使用这个关键字,我们可以确保类内部的变量不能被修改。另一个讨论是应用第一类和纯函数--你在上一章中学到的东西--以获得不可变性的力量。
我们还创建了可变类,然后将其重构为不可变类。我们现在能够区分可变和不可变对象,并将其应用于我们的函数式代码中。最后,在本章中,我们列举了不可变对象的好处,因此我们有信心在我们的日常代码中使用它。
现在我们的头脑中可能会出现另一个问题。如果我们必须处理不可变对象,我们如何运行递归呢?我们甚至不能在方法中修改一个变量。在下一章中,我们将通过讨论函数式编程中的递归来解决这个问题。
第四章:使用递归算法重复方法调用
在上一章中,您了解了使我们不处理副作用的不可变状态。在本章中,让我们来看看递归的概念。作为面向对象编程的程序员,我们通常使用迭代来重复过程,而不是递归。然而,递归比迭代更有益。例如,一些问题(尤其是数学问题)使用递归更容易解决,而且幸运的是,所有算法都可以递归地定义。这使得可视化和证明变得更加容易。要了解更多关于递归的知识,本章将讨论以下主题:
-
迭代和递归调用的区别
-
重复不可变函数的调用
-
在递归中找到更好的方法,使用尾递归
-
列举三种递归--函数式、过程式和回溯递归
递归地重复函数调用
作为程序员,尤其是在面向对象编程中,我们通常使用迭代技术来重复我们的过程。现在,我们将讨论递归方法来重复我们的过程,并在功能方法中使用它。基本上,递归和迭代执行相同的任务,即逐步解决复杂的任务,然后将结果组合起来。然而,它们有所不同。迭代过程强调我们应该不断重复过程,直到任务完成,而递归强调需要将任务分解成更小的部分,直到我们能够解决任务,然后将结果组合起来。当我们需要运行某个过程直到达到限制或读取流直到达到eof()
时,我们可以使用迭代过程。此外,递归在某些情况下可以提供最佳值,例如在计算阶乘时。
执行迭代过程来重复过程
我们将从迭代过程开始。正如我们之前讨论过的,阶乘的计算如果使用递归方法设计会更好。然而,也可以使用迭代方法来设计。在这里,我们将有一个名为factorial_iteration_do_while.cpp
的代码,我们可以用它来计算阶乘。我们将有一个名为factorial()
的函数,它传递一个参数,将计算我们在参数中传递的阶乘值。代码应该如下所示:
/* factorial_iteration_do_while.cpp */
#include <iostream>
using namespace std;
// Function containing
// do-while loop iteration
int factorial (int n)
{
int result = 1;
int i = 1;
// Running iteration using do-while loop
do
{
result *= i;
}
while(++i <= n);
return result;
}
auto main() -> int
{
cout << "[factorial_iteration_do_while.cpp]" << endl;
// Invoking factorial() function nine times
for(int i = 1; i < 10; ++i)
{
cout << i << "! = " << factorial(i) << endl;
}
return 0;
}
正如我们在先前的代码中所看到的,我们依赖于我们传递给factorial()
函数的n
的值,来确定将发生多少次迭代。每次迭代执行时,result
变量将与计数器i
相乘。最后,result
变量将通过组合迭代的结果值来保存最后的结果。我们应该在屏幕上得到以下输出:
迭代中的另一种技术是使用另一个迭代过程。我们可以重构先前的代码,使用for
循环在factorial()
函数中。以下是从我们先前的factorial_iteration_do_while.cpp
代码重构而来的factorial_iteration_for.cpp
代码:
/* factorial_iteration_do_while.cpp */
#include <iostream>
using namespace std;
// Function containing
// for loop iteration
int factorial (int n)
{
int result = 1;
// Running iteration using for loop
for(int i = 1; i <= n; ++i)
{
result *= i;
}
return result;
}
auto main() -> int
{
cout << "[factorial_iteration_for.cpp]" << endl;
// Invoking factorial() function nine times
for(int i = 1; i < 10; ++i)
{
cout << i << "! = " << factorial(i) << endl;
}
return 0;
}
正如我们所看到的,我们用for
循环替换了do-while
循环。然而,程序的行为将完全相同,因为它也会每次迭代执行时将当前结果与i
计数器相乘。在这个迭代结束时,我们将从这个乘法过程中获得最终结果。屏幕应该显示以下输出:
现在我们已经成功地使用迭代来实现了阶乘目的,可以使用do-while
或for
循环。
当我们尝试将do-while
循环重构为for
循环时,看起来太琐碎了。我们可能知道,for
循环允许我们在知道要运行多少次时运行循环,而do-while
循环在我们放入其中以及何时停止时给我们更大的灵活性,例如while(i > 0)
或使用布尔值,如while(true)
。然而,根据前面的例子,我们现在可以说我们可以将for
循环或do-while
循环切换为递归。
执行递归过程以重复该过程
我们之前讨论过,递归在函数式编程中具有更好的性能。我们还以迭代方式开发了factorial()
函数。现在,让我们将之前的代码重构为factorial_recursion.cpp
,它将使用递归方法而不是迭代方法。该代码将执行与我们之前的代码相同的任务。但是,我们将修改factorial()
函数,使其在函数末尾调用自身。代码如下所示:
/* factorial_recursion.cpp */
#include <iostream>
using namespace std;
int factorial(int n)
{
// Running recursion here
if (n == 0)
return 1;
else
return n * factorial (n - 1);
}
auto main() -> int
{
cout << "[factorial_recursion.cpp]" << endl;
for(int i = 1; i < 10; ++i)
{
cout << i << "! = " << factorial(i) << endl;
}
return 0;
}
正如我们所看到的,在前面的代码中,factorial()
函数调用自身直到n
为0
。每次函数调用自身时,它会减少n
参数。当传递的参数为0
时,函数将立即返回1
。与我们之前的两个代码块相比,我们也将得到相同的输出,如下面的屏幕截图所示:
尽管递归为我们提供了易于维护代码所需的简单性,但我们必须注意我们传递给递归函数的参数。例如,在factorial_recursion.cpp
代码中的factorial()
函数中,如果我们将负数传递给n < 0
函数,我们将得到无限循环,并且可能会导致设备崩溃。
重复不可变函数
正如我们在前一章中讨论的,我们需要递归循环不可变函数。假设我们有一个不可变的fibonacci()
函数。然后,我们需要将其重构为递归函数。fibonacci_iteration.cpp
代码以迭代方式实现了fibonacci()
函数。代码如下所示:
/* fibonacci_iteration.cpp */
#include <iostream>
using namespace std;
// Function for generating
// Fibonacci sequence using iteration
int fibonacci(int n)
{
if (n == 0)
return 0;
int previous = 0;
int current = 1;
for (int i = 1; i < n; ++i)
{
int next = previous + current;
previous = current;
current = next;
}
return current;
}
auto main() -> int
{
cout << "[fibonacci_iteration.cpp]" << endl;
// Invoking fibonacci() function ten times
for(int i = 0; i < 10; ++i)
{
cout << fibonacci(i) << " ";
}
cout << endl;
return 0;
}
正如我们在前面的代码中所看到的,fibonacci()
函数是不可变的,因为每次它获得相同的n
输入时都会返回相同的值。输出应该如下屏幕截图所示:
如果我们需要将其重构为递归函数,我们可以使用以下fibonacci_recursion.cpp
代码:
/* fibonacci_recursion.cpp */
#include <iostream>
using namespace std;
// Function for generating
// Fibonacci sequence using recursion
int fibonacci(int n)
{
if(n <= 1)
return n;
return fibonacci(n-1) + fibonacci(n-2);
}
auto main() -> int
{
cout << "[fibonacci_recursion.cpp]" << endl;
// Invoking fibonacci() function ten times
for(int i = 0; i < 10; ++i)
{
cout << fibonacci(i) << " ";
}
cout << endl;
return 0;
}
正如我们所看到的,前面的代码采用了递归方法,因为它在函数末尾调用函数本身。现在我们有了递归fibonacci()
函数,它将在控制台上给出以下输出:
现在,与fibonacci_iteration.cpp
代码相比,fibonacci_recursion.cpp
代码显示了完全相同的输出。
接近尾递归
当递归调用在函数末尾执行时,发生尾递归。它被认为比我们之前开发的非尾递归代码更好,因为编译器可以更好地优化代码。由于递归调用是函数执行的最后一个语句,因此在此函数中没有更多的事情要做。结果是编译器不需要保存当前函数的堆栈帧。让我们看看以下tail_recursion.cpp
代码实现尾递归:
/* tail_recursion.cpp */
#include <iostream>
using namespace std;
void displayNumber(long long n)
{
// Displaying the current n value
cout << n << endl;
// The last executed statement
// is the recursive call
displayNumber(n + 1);
}
auto main() -> int
{
cout << "[tail_recursion.cpp]" << endl;
// Invoking the displayNumber() function
// containing tail recursion
displayNumber(0);
return 0;
}
正如我们在前面的代码中所看到的,displayNumber()
函数是一个尾递归调用函数,因为它在过程结束时调用自身。确实,如果运行前述的tail_recursion.cpp
代码,程序将不会结束,因为它会增加displayNumber()
函数中的n
的值。当n
的值达到long long
数据类型的最大值时,程序可能会崩溃。然而,由于尾递归不会在堆栈中存储值,程序将不会出现堆栈问题(堆栈溢出)。
此外,我们还可以重构tail_recursion.cpp
代码中的前述displayNumber()
函数,使用goto
关键字而不是一遍又一遍地调用函数。重构后的代码可以在以下tail_recursion_goto.cpp
代码中看到:
/* tail_recursion_goto.cpp */
#include <iostream>
using namespace std;
void displayNumber(long long n)
{
loop:
// Displaying the current n value
cout << n << endl;
// Update parameters of recursive call
// and replace recursive call with goto
n++;
goto loop;
}
auto main() -> int
{
cout << "[tail_recursion_goto.cpp]" << endl;
// Invoking the displayNumber() function
// containing tail recursion
displayNumber(0);
return 0;
}
在前面的代码中,我们可以看到,可以使用goto
关键字在displayNumber()
函数中删除最后一个调用。这就是编译器通过执行尾调用消除来优化尾递归的方式,它将用goto
关键字替换最后一个调用。我们还会看到,在displayNumber()
函数中不需要堆栈。
不要忘记使用编译器提供的优化选项编译包含尾递归的代码。由于我们使用 GCC,始终启用优化级别 2(-O2
)以获得优化的代码。未启用优化编译的效果是,我们前面的两个程序(tail_recursion.cpp
和tail_recursion_goto.cpp
)将因堆栈溢出问题而崩溃。有关 GCC 中优化选项的更多信息,请查看gcc.gnu.org/onlinedocs/gcc-7.1.0/gcc/Optimize-Options.html
。
现在,让我们创建一个有用的尾递归调用。在前一节中,我们已经成功地将迭代函数重构为递归函数。factorial()
函数现在已经成为一个递归函数,并在函数末尾调用自身。然而,它并不是尾递归,尽管函数在函数末尾调用自身。如果我们仔细观察,factorial(n-1)
返回的值被factorial(n)
使用,所以对factorial(n-1)
的调用不是factorial(n)
所做的最后一件事。
我们可以将我们的factorial_recursion.cpp
代码改为尾递归函数。我们将开发以下factorial_recursion_tail.cpp
代码,修改factorial()
函数,并添加一个名为factorialTail()
的新函数。代码如下所示:
/* factorial_recursion_tail.cpp */
#include <iostream>
using namespace std;
// Function for calculating factorial
// tail recursion
int factorialTail(int n, int i)
{
if (n == 0)
return i;
return factorialTail(n - 1, n * i);
}
// The caller of tail recursion function
int factorial(int n)
{
return factorialTail(n, 1);
}
auto main() -> int
{
cout << "[factorial_recursion_tail.cpp]" << endl;
// Invoking fibonacci() function ten times
for(int i = 1; i < 10; ++i)
{
cout << i << "! = " << factorial(i) << endl;
}
return 0;
}
正如我们所看到的,我们已经将factorial()
函数从factorial_recursion.cpp
代码移动到factorial_recursion_tail.cpp
代码中的factorialTail()
函数,该函数需要两个参数。结果是,在我们调用factorial(i)
之后,它将调用factorialTail()
函数。在这个函数的末尾,只有factorialTail()
函数被调用。以下图片是factorial_recursion_tail.cpp
代码的输出,与factorial_recursion.cpp
代码完全相同。这也证明我们已成功将factorial_recursion.cpp
代码重构为尾递归。
熟悉函数式、过程式和回溯递归。
现在我们已经了解了一点关于递归,递归函数将在其内部调用自身。递归只有在达到一定值时才会停止。我们将立即讨论三种类型的递归--功能递归,过程递归和回溯递归;然而,这三种递归可能不是标准术语。功能递归是一个返回某个值的递归过程。过程递归是一个不返回值的递归过程,但在每次递归中执行动作。回溯递归是一个将任务分解为一小组子任务的递归过程,如果它们不起作用,可以取消。让我们在下面的讨论中考虑这些递归类型。
期待从功能递归中得到结果
在功能递归中,该过程试图通过递归地组合子问题的结果来解决问题。我们组合的结果来自子问题的返回值。假设我们有一个计算一个数的幂的问题,例如,2
的2
次方是4
(2² = 4
)。通过使用迭代,我们可以构建一个像下面的exponential_iteration.cpp
代码的代码。我们有一个名为power()
的函数,它将通过两个参数--base
和exp
来传递。符号将是base^(exp)
,代码看起来像这样:
/* exponential_iteration.cpp */
#include <iostream>
using namespace std;
// Calculating the power of number
// using iteration
int power(int base, int exp)
{
int result = 1;
for(int i = 0; i < exp; ++i)
{
result *= base;
}
return(result);
}
auto main() -> int
{
cout << "[exponential_iteration.cpp]" << endl;
// Invoking power() function six times
for(int i = 0; i <= 5; ++i)
{
cout << "power (2, " << i << ") = ";
cout << power(2, i) << endl;
}
return 0;
}
正如我们在前面的代码中所看到的,我们首先使用迭代版本,然后再使用递归版本,因为我们通常在日常生活中最常使用迭代。我们通过将result
值在每次迭代中乘以base
值来组合结果。如果我们运行上面的代码,我们将在控制台上得到以下输出:
现在,让我们将我们之前的代码重构为递归版本。我们将有exponential_recursion.cpp
代码,它将具有相同的power()
函数签名。然而,我们将不使用for
循环,而是使用递归,函数在函数的末尾调用自身。代码应该写成如下所示:
/* exponential_recursion.cpp */
#include <iostream>
using namespace std;
// Calculating the power of number
// using recursion
int power(int base, int exp)
{
if(exp == 0)
return 1;
else
return base * power(base, exp - 1);
}
auto main() -> int
{
cout << "[exponential_recursion.cpp]" << endl;
// Invoking power() function six times
for(int i = 0; i <= 5; ++i)
{
cout << "power (2, " << i << ") = ";
cout << power(2, i) << endl;
}
return 0;
}
正如我们之前讨论的,功能递归返回值,power()
函数是一个功能递归,因为它返回int
值。我们将从每个子函数返回的值得到最终结果。因此,我们将在控制台上得到以下输出:
在过程递归中递归运行任务
因此,我们有一个期望从函数中得到返回值的功能递归。有时,我们不需要返回值,因为我们在函数内部运行任务。为了实现这个目的,我们可以使用过程递归。假设我们想要对一个短字符串进行排列,以找到它的所有可能的排列。我们只需要在每次递归执行时打印结果,而不需要返回值。
我们有以下的permutation.cpp
代码来演示这个任务。它有一个permute()
函数,将被调用一次,然后它将递归地调用doPermute()
函数。代码应该写成如下所示:
/* permutation.cpp */
#include <iostream>
using namespace std;
// Calculation the permutation
// of the given string
void doPermute(
const string &chosen,
const string &remaining)
{
if(remaining == "")
{
cout << chosen << endl;
}
else
{
for(uint32_t u = 0; u < remaining.length(); ++u)
{
doPermute(
chosen + remaining[u],
remaining.substr(0, u)
+ remaining.substr(u + 1));
}
}
}
// The caller of doPermute() function
void permute(
const string &s)
{
doPermute("", s);
}
auto main() -> int
{
cout << "[permutation.cpp]" << endl;
// Initializing str variable
// then ask user to fill in
string str;
cout << "Permutation of a string" << endl;
cout << "Enter a string: ";
getline(cin, str);
// Finding the possibility of the permutation
// by calling permute() function
cout << endl << "The possibility permutation of ";
cout << str << endl;
permute(str);
return 0;
}
正如我们在前面的代码中所看到的,我们要求用户输入一个字符串,然后代码将使用permute()
函数找到这个排列的可能性。它将从doPermute()
中的空字符串开始,因为来自用户的给定字符串也是可能的。控制台上的输出应该如下所示:
回溯递归
正如我们之前讨论的,如果子任务不起作用,我们可以撤消这个过程。让我们尝试一个迷宫,我们必须找到从起点到终点的路。假设我们必须找到从S
到F
的路,就像下面的迷宫一样:
# # # # # # # #
# S #
# # # # # # #
# # # # # #
# #
# # # # # # #
# F #
# # # # # # # #
为了解决这个问题,我们必须决定我们需要的路线,以找到终点。但是,我们将假设每个选择都是好的,直到我们证明它不是。递归将返回一个布尔值,以标记它是否是正确的方式。如果我们选择了错误的方式,调用堆栈将解开,并且将撤消选择。首先,我们将在我们的代码中绘制labyrinth
。在以下代码中,将会有createLabyrinth()
和displayLabyrinth()
函数。代码看起来像这样:
/* labyrinth.cpp */
#include <iostream>
#include <vector>
using namespace std;
vector<vector<char>> createLabyrinth()
{
// Initializing the multidimensional vector
// labyrinth
// # is a wall
// S is the starting point
// E is the finishing point
vector<vector<char>> labyrinth =
{
{'#', '#', '#', '#', '#', '#', '#', '#'},
{'#', 'S', ' ', ' ', ' ', ' ', ' ', '#'},
{'#', '#', '#', ' ', '#', '#', '#', '#'},
{'#', ' ', '#', ' ', '#', '#', '#', '#'},
{'#', ' ', ' ', ' ', ' ', ' ', ' ', '#'},
{'#', ' ', '#', '#', '#', '#', '#', '#'},
{'#', ' ', ' ', ' ', ' ', ' ', 'F', '#'},
{'#', '#', '#', '#', '#', '#', '#', '#'}
};
return labyrinth;
}
void displayLabyrinth(vector<vector<char>> labyrinth)
{
cout << endl;
cout << "====================" << endl;
cout << "The Labyrinth" << endl;
cout << "====================" << endl;
// Displaying all characters in labyrinth vector
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
cout << labyrinth[i][j] << " ";
}
cout << endl;
}
cout << "====================" << endl << endl;
}
auto main() -> int
{
vector<vector<char>> labyrinth = createLabyrinth();
displayLabyrinth(labyrinth);
string line;
cout << endl << "Press enter to continue..." << endl;
getline(cin, line);
return 0;
}
正如我们所看到的,前面的代码中没有递归。createLabyrinth()
函数只是创建一个包含labyrinth
模式的二维数组,而displayLabyrinth()
只是将数组显示到控制台。如果我们运行前面的代码,我们将在控制台上看到以下输出:
从前面的截图中,我们可以看到有两个点--S
是起点,F
是终点。代码必须找到从S
到F
的路径。预期的路线应该如下:
在前面的截图中,白色箭头是我们期望从S
到达F
的路径。现在,让我们开发解决这个迷宫问题的代码。我们将创建一个名为navigate
的函数,通过确定以下三种状态来找到可能的路线:
-
如果我们在[x,y]位置找到
F
,例如labyrinth[2][4]
,那么我们已经解决了问题,只需返回true
作为返回值。 -
如果[x,y]位置是
#
,这意味着我们面对墙壁,必须重新访问其他[x,y]位置。 -
否则,我们在该位置打印
*
来标记我们已经访问过它。
在分析了三种状态之后,我们将从递归情况开始:
-
如果路径搜索器可以导航到
row - 1
,并且大于或等于0
(row - 1 >= 0 && navigate(labyrinth, row - 1, col)
),它将向上移动 -
如果路径搜索器可以导航到
row + 1
,并且小于8
(row + 1 < 8 && navigate(labyrinth, row + 1, col)
),它将向下移动 -
如果路径搜索器可以导航到
col - 1
,并且大于或等于0
(col - 1 >= 0 && navigate(labyrinth, row, col - 1)
),它将向左移动 -
如果路径搜索器可以导航到
col + 1
,并且小于8
(col + 1 < 8 && navigate(labyrinth, row, col + 1)
),它将向右移动
我们将有以下navigate()
函数:
bool navigate(
vector<vector<char>> labyrinth,
int row,
int col)
{
// Displaying labyrinth
displayLabyrinth(labyrinth);
cout << "Checking cell (";
cout << row << "," << col << ")" << endl;
// Pause 1 millisecond
// before navigating
sleep(1);
if (labyrinth[row][col] == 'F')
{
cout << "Yeayy.. ";
cout << "Found the finish flag ";
cout << "at point (" << row << ",";
cout << col << ")" << endl;
return (true);
}
else if (
labyrinth[row][col] == '#' ||
labyrinth[row][col] == '*')
{
return (false);
}
else if (labyrinth[row][col] == ' ')
{
labyrinth[row][col] = '*';
}
if ((row + 1 < rows) &&
navigate(labyrinth, row + 1, col))
return (true);
if ((col + 1 < cols) &&
navigate(labyrinth, row, col + 1))
return (true);
if ((row - 1 >= 0) &&
navigate(labyrinth, row - 1, col))
return (true);
if ((col - 1 >= 0) &&
navigate(labyrinth, row, col - 1))
return (true);
return (false);
}
现在我们有了navigate()
函数来找出正确的路径以找到F
。但是,在运行navigate()
函数之前,我们必须确保S
在那里。然后我们必须开发名为isLabyrinthSolvable()
的辅助函数。它将循环遍历迷宫数组,并告知S
是否存在。以下代码片段是isLabyrinthSolvable()
函数的实现:
bool isLabyrinthSolvable(
vector<vector<char>> labyrinth)
{
int start_row = -1;
int start_col = -1;
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
if (labyrinth[i][j] == 'S')
{
start_row = i;
start_col = j;
break;
}
}
}
if (start_row == -1 || start_col == -1)
{
cout << "No valid starting point found!" << endl;
return (false);
}
cout << "Starting at point (" << start_row << ",";
cout << start_col << ")" << endl;
return navigate(labyrinth, start_row, start_col);
}
正如我们在前面的代码片段中所看到的,我们提到了rows
和cols
变量。我们将它们初始化为全局变量,就像我们在以下代码片段中所看到的那样:
const int rows = 8;
const int cols = 8;
现在,让我们看一下以下代码,如果我们将navigate()
和isLabyrinthSolvable()
函数插入到labyrinth.cpp
代码中:
/* labyrinth.cpp */
#include <iostream>
#include <vector>
#include <unistd.h>
using namespace std;
const int rows = 8;
const int cols = 8;
vector<vector<char>> createLabyrinth()
{
// Initializing the multidimensional vector
// labyrinth
// # is a wall
// S is the starting point
// E is the finishing point
vector<vector<char>> labyrinth =
{
{'#', '#', '#', '#', '#', '#', '#', '#'},
{'#', 'S', ' ', ' ', ' ', ' ', ' ', '#'},
{'#', '#', '#', ' ', '#', '#', '#', '#'},
{'#', ' ', '#', ' ', '#', '#', '#', '#'},
{'#', ' ', ' ', ' ', ' ', ' ', ' ', '#'},
{'#', ' ', '#', '#', '#', '#', '#', '#'},
{'#', ' ', ' ', ' ', ' ', ' ', 'F', '#'},
{'#', '#', '#', '#', '#', '#', '#', '#'}
};
return labyrinth;
}
void displayLabyrinth(
vector<vector<char>> labyrinth)
{
cout << endl;
cout << "====================" << endl;
cout << "The Labyrinth" << endl;
cout << "====================" << endl;
// Displaying all characters in labyrinth vector
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
cout << labyrinth[i][j] << " ";
}
cout << endl;
}
cout << "====================" << endl << endl;
}
bool navigate(
vector<vector<char>> labyrinth,
int row,
int col)
{
// Displaying labyrinth
displayLabyrinth(labyrinth);
cout << "Checking cell (";
cout << row << "," << col << ")" << endl;
// Pause 1 millisecond
// before navigating
sleep(1);
if (labyrinth[row][col] == 'F')
{
cout << "Yeayy.. ";
cout << "Found the finish flag ";
cout << "at point (" << row << ",";
cout << col << ")" << endl;
return (true);
}
else if (
labyrinth[row][col] == '#' ||
labyrinth[row][col] == '*')
{
return (false);
}
else if (labyrinth[row][col] == ' ')
{
labyrinth[row][col] = '*';
}
if ((row + 1 < rows) &&
navigate(labyrinth, row + 1, col))
return (true);
if ((col + 1 < cols) &&
navigate(labyrinth, row, col + 1))
return (true);
if ((row - 1 >= 0) &&
navigate(labyrinth, row - 1, col))
return (true);
if ((col - 1 >= 0) &&
navigate(labyrinth, row, col - 1))
return (true);
return (false);
}
bool isLabyrinthSolvable(
vector<vector<char>> labyrinth)
{
int start_row = -1;
int start_col = -1;
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
if (labyrinth[i][j] == 'S')
{
start_row = i;
start_col = j;
break;
}
}
}
if (start_row == -1 || start_col == -1)
{
cerr << "No valid starting point found!" << endl;
return (false);
}
cout << "Starting at point (" << start_row << ",";
cout << start_col << ")" << endl;
return navigate(labyrinth, start_row, start_col);
}
auto main() -> int
{
vector<vector<char>> labyrinth = createLabyrinth();
displayLabyrinth(labyrinth);
string line;
cout << endl << "Press enter to continue..." << endl;
getline(cin, line);
if (isLabyrinthSolvable(labyrinth))
cout << "Labyrinth solved!" << endl;
else
cout << "Labyrinth could not be solved!" << endl;
return 0;
}
正如我们在前面的引用中所看到的,在main()
函数中,我们首先运行isLabyrinthSolvable()
函数,然后调用navigate()
函数。navigate()
函数将通过迷宫找出正确的路径。以下是代码的输出:
然而,如果我们追踪程序如何解决迷宫,当它找到终点时,它会面临错误的路线,就像我们在以下截图中所看到的:
摘要
正如我们所看到的,在前面的截图中有一个白色的方块。当它寻找正确的路径时,这是错误的选择。一旦遇到障碍,它就会返回并寻找其他方法。它还会撤消它所做的选择。让我们看看下面的截图,它向我们展示了当递归找到另一条路线并撤消先前的选择时:
在前面的截图中,我们可以看到递归尝试另一条路线,之前失败的路线已经消失,因为回溯递归取消了该路线。递归现在有了正确的路径,它可以继续直到找到终点旗。因此,我们现在成功地开发了回溯递归。
本章为我们提供了使用迭代和递归重复函数调用的技术。然而,由于递归比迭代更加功能化,我们强调了对递归而不是迭代的讨论。我们从迭代和递归的区别开始。然后我们继续讨论了重构不可变函数以成为递归不可变函数。
在学习了递归之后,我们发现了其他更好的递归技术。我们还讨论了尾递归以获得这种改进的技术。最后,我们列举了三种递归--功能递归、过程递归和回溯递归。当我们期望递归的返回值时,通常使用功能递归。否则,我们使用过程递归。如果我们需要分解问题并在递归不起作用时撤消递归性能,我们可以使用回溯递归来解决问题。
在下一章中,我们将讨论延迟评估以使代码运行更快。这将使代码变得更有效,因为它将确保不必要的代码不会被执行。
第五章:使用懒惰评估拖延执行过程
在前一章中,我们讨论了在函数式方法中重复函数调用的递归。现在,我们将讨论懒惰评估,它可以使我们的代码变得更加高效,因为它只在我们需要时才运行。我们还将应用递归,这是我们在前一章中讨论过的话题,以生成懒惰代码。
在本章中,我们讨论懒惰评估,以使代码运行更快。这将使代码变得高效,因为它将确保不必要的代码不会被执行。以下是我们将讨论的主题,以深入了解懒惰评估:
-
区分急切和懒惰评估之间的差异
-
使用缓存技术优化代码
-
将急切评估重构为懒惰评估
-
设计有用的类,可以在其他的函数式代码中重复使用
评估表达式
每种编程语言都有其确定何时评估函数调用的参数以及必须传递给参数的值类型的策略。在编程语言中,有两种主要使用的策略评估--严格(急切)评估和非严格(懒惰)评估。
立即运行表达式进行严格评估
严格评估在大多数命令式编程语言中使用。它将立即执行我们的代码。假设我们有以下方程:
int i = (x + (y * z));
在严格评估中,最内层的括号将首先计算,然后向外计算前面的方程。这意味着我们将计算y * z
,然后将结果加到x
上。为了更清楚,让我们看看以下的strict.cpp
代码:
/* strict.cpp */
#include <iostream>
using namespace std;
int OuterFormula(int x, int yz)
{
// For logging purpose only
cout << "Calculate " << x << " + ";
cout << "InnerFormula(" << yz << ")";
cout << endl;
// Returning the calculation result
return x * yz;
}
int InnerFormula(int y, int z)
{
// For logging purpose only
cout << "Calculate " << y << " * ";
cout << z << endl;
// Returning the calculation result
return y * z;
}
auto main() -> int
{
cout << "[strict.cpp]" << endl;
// Initializing three int variables
// for the calculation
int x = 4;
int y = 3;
int z = 2;
// Calculating the expression
cout << "Calculate " << x <<" + ";
cout << "(" << y << " * " << z << ")";
cout << endl;
int result = OuterFormula(x, InnerFormula(y, z));
// For logging purpose only
cout << x << " + ";
cout << "(" << y << " * " << z << ")";
cout << " = " << result << endl;
return 0;
}
正如我们之前讨论的,前面代码的执行将首先是y * z
,然后我们将结果加到x
上,正如我们在以下输出中所看到的:
前面的执行顺序是我们通常期望的。然而,在非严格评估中,我们将重新安排这个执行过程。
使用非严格评估延迟表达式
在非严格评估中,+
运算符首先被简化,然后我们简化内部公式,即(y * z)
。我们将看到评估将从外到内开始。我们将重构我们之前的strict.cpp
代码,使其成为非严格评估。代码应该像以下的non_strict.cpp
代码:
/* non_strict.cpp */
#include <functional>
#include <iostream>
using namespace std;
int OuterFormulaNonStrict(
int x,
int y,
int z,
function<int(int, int)> yzFunc)
{
// For logging purpose only
cout << "Calculate " << x << " + ";
cout << "InnerFormula(" << y << ", ";
cout << z << ")" << endl;
// Returning the calculation result
return x * yzFunc(y, z);
}
int InnerFormula(int y, int z)
{
// For logging purpose only
cout << "Calculate " << y << " * ";
cout << z << endl;
// Returning the calculation result
return y * z;
}
auto main() -> int
{
cout << "[non_strict.cpp]" << endl;
// Initializing three int variables
// for the calculation
int x = 4;
int y = 3;
int z = 2;
// Calculating the expression
cout << "Calculate " << x <<" + ";
cout << "(" << y << " * " << z << ")";
cout << endl;
int result = OuterFormulaNonStrict(x, y, z, InnerFormula);
// For logging purpose only
cout << x << " + ";
cout << "(" << y << " * " << z << ")";
cout << " = " << result << endl;
return 0;
}
正如我们所看到的,我们将strict.cpp
代码中的OuterFormula()
函数修改为non_strict.cpp
代码中的OuterFormulaNonStrict()
函数。在OuterFormulaNonStrict()
函数中,我们除了三个变量x
、y
和z
之外,还将一个函数作为参数传递。因此,前面表达式的执行顺序发生了变化。当我们运行non_strict.cpp
代码时,我们应该在控制台屏幕上看到以下内容:
从前面的输出中,我们已经证明我们的代码正在执行非严格评估,因为它现在首先计算加法运算符(+
)而不是乘法(*
)。然而,结果仍然是正确的,尽管顺序已经改变。
懒惰评估的基本概念
在创建懒惰代码之前,让我们讨论懒惰评估的基本概念。我们将使用延迟过程使我们的代码变得懒惰,使用缓存技术来增加代码的性能,避免不必要的计算,以及优化技术,通过存储昂贵的函数调用的结果并在再次出现相同的输入时返回缓存的结果来加快代码的速度。在我们看完这些技术之后,我们将尝试开发真正的懒惰代码。
延迟过程
懒惰的基本概念是延迟一个过程。在本节中,我们将讨论如何延迟特定过程的执行。我们将创建一个名为Delay
的新类。当我们构造类时,我们将把一个函数传递给它。除非我们调用Fetch()
方法,否则函数不会运行。函数的实现如下:
template<class T> class Delay
{
private:
function<T()> m_func;
public:
Delay(
function<T()> func)
: m_func(func)
{
}
T Fetch()
{
return m_func();
}
};
现在,让我们使用Delay
类来推迟执行。我们将创建一个名为delaying.cpp
的文件,其中将运行两个函数--multiply
和division
。然而,只有在调用Fetch()
方法之后,这两个函数才会被运行。文件的内容如下:
/* delaying.cpp */
#include <iostream>
#include <functional>
using namespace std;
template<class T> class Delay
{
private:
function<T()> m_func;
public:
Delay(function<T()> func) : m_func(func)
{
}
T Fetch()
{
return m_func();
}
};
auto main() -> int
{
cout << "[delaying.cpp]" << endl;
// Initializing several int variables
int a = 10;
int b = 5;
cout << "Constructing Delay<> named multiply";
cout << endl;
Delay<int> multiply([a, b]()
{
cout << "Delay<> named multiply";
cout << " is constructed." << endl;
return a * b;
});
cout << "Constructing Delay<> named division";
cout << endl;
Delay<int> division([a, b]()
{
cout << "Delay<> named division ";
cout << "is constructed." << endl;
return a / b;
});
cout << "Invoking Fetch() method in ";
cout << "multiply instance." << endl;
int c = multiply.Fetch();
cout << "Invoking Fetch() method in ";
cout << "division instance." << endl;
int d = division.Fetch();
// Displaying the result
cout << "The result of a * b = " << c << endl;
cout << "The result of a / b = " << d << endl;
return 0;
}
正如我们在第一章中讨论的,深入现代 C++,我们可以使用 Lambda 表达式来构建multiply
和division
函数,然后将它们传递给每个Delay
构造函数。在这个阶段,函数还没有运行。它将在调用Fetch()
方法后运行--multiply.Fetch()
和division.Fetch()
。我们将在屏幕上看到以下的输出截图:
正如我们在前面的输出截图中所看到的,当调用Fetch()
方法时,multiply
和division
实例被构造(见两个白色箭头),而不是在调用Delay
类的构造函数时。现在,我们已经成功地延迟了执行,并且我们可以说只有在需要时才执行这个过程。
使用记忆化技术缓存值
我们现在已经成功地延迟了通过消耗Delay
类来执行函数。然而,由于每次调用Fetch()
方法时Delay
类实例的函数将被运行,如果函数不是纯函数或具有副作用,可能会出现意外结果。让我们通过修改multiply
函数来重构我们之前的delaying.cpp
代码。这个函数现在变成了一个非纯函数,因为它依赖于外部变量。代码应该是这样的:
/* delaying_non_pure.cpp */
#include <iostream>
#include <functional>
using namespace std;
template<class T> class Delay
{
private:
function<T()> m_func;
public:
Delay(function<T()> func) : m_func(func)
{
}
T Fetch()
{
return m_func();
}
};
auto main() -> int
{
cout << "[delaying_non_pure.cpp]" << endl;
// Initializing several int variables
int a = 10;
int b = 5;
int multiplexer = 0;
// Constructing Delay<> named multiply_impure
Delay<int> multiply_impure([&]()
{
return multiplexer * a * b;
});
// Invoking Fetch() method in multiply_impure instance
// multiple times
for (int i = 0; i < 5; ++i)
{
++multiplexer;
cout << "Multiplexer = " << multiplexer << endl;
cout << "a * b = " << multiply_impure.Fetch();
cout << endl;
}
return 0;
}
正如我们在前面的代码中所看到的,我们现在有一个名为multiply_impure
的新 Lambda 表达式,这是我们在delaying.cpp
代码中创建的multiply
函数的重构版本。multiply_impure
函数依赖于multiplexer
变量,其值将在我们调用Fetch()
方法之前每次增加。我们应该在屏幕上看到以下的截图输出:
正如我们所看到的,Fetch()
方法每次被调用时都会给出不同的结果。我们现在必须重构Delay
类,以确保每次Fetch()
方法运行函数时都返回相同的结果。为了实现这一点,我们将使用记忆化技术,它存储函数调用的结果,并在再次出现相同的输入时返回缓存的结果。
我们将Delay
类重命名为Memoization
类。这不仅会延迟函数调用,还会记录具有特定传递参数的函数。因此,下一次具有这些参数的函数发生时,函数本身将不会运行,而只会返回缓存的结果。为了方便我们的讨论,让我们来看一下以下的Memoization
类实现:
template<class T> class Memoization
{
private:
T const & (*m_subRoutine)(Memoization *);
mutable T m_recordedFunc;
function<T()> m_func;
static T const & ForceSubroutine(Memoization * d)
{
return d->DoRecording();
}
static T const & FetchSubroutine(Memoization * d)
{
return d->FetchRecording();
}
T const & FetchRecording()
{
return m_recordedFunc;
}
T const & DoRecording()
{
m_recordedFunc = m_func();
m_subRoutine = &FetchSubroutine;
return FetchRecording();
}
public:
Memoization(function<T()> func) : m_func(func),
m_subRoutine(&ForceSubroutine),
m_recordedFunc(T())
{
}
T Fetch()
{
return m_subRoutine(this);
}
};
正如我们在前面的代码片段中所看到的,我们现在有FetchRecording()
和DoRecording()
来获取和设置我们存储的函数。此外,当类被构造时,它将记录传递的函数并将其保存到m_subRoutine
中。当调用Fetch()
方法时,类将检查m_subRoutine
,并查找它是否具有当前传递参数的函数值。如果是,它将简单地返回m_subRoutine
中的值,而不是运行函数。现在,让我们看一下以下的delaying_non_pure_memoization.cpp
代码,它使用Memoization
类:
/* delaying_non_pure_memoization.cpp */
#include <iostream>
#include <functional>
using namespace std;
template<class T> class Memoization
{
private:
T const & (*m_subRoutine)(Memoization *);
mutable T m_recordedFunc;
function<T()> m_func;
static T const & ForceSubroutine(Memoization * d)
{
return d->DoRecording();
}
static T const & FetchSubroutine(Memoization * d)
{
return d->FetchRecording();
}
T const & FetchRecording()
{
return m_recordedFunc;
}
T const & DoRecording()
{
m_recordedFunc = m_func();
m_subRoutine = &FetchSubroutine;
return FetchRecording();
}
public:
Memoization(function<T()> func) : m_func(func),
m_subRoutine(&ForceSubroutine),
m_recordedFunc(T())
{
}
T Fetch()
{
return m_subRoutine(this);
}
};
auto main() -> int
{
cout << "[delaying_non_pure_memoization.cpp]" << endl;
// Initializing several int variables
int a = 10;
int b = 5;
int multiplexer = 0;
// Constructing Memoization<> named multiply_impure
Memoization<int> multiply_impure([&]()
{
return multiplexer * a * b;
});
// Invoking Fetch() method in multiply_impure instance
// multiple times
for (int i = 0; i < 5; ++i)
{
++multiplexer;
cout << "Multiplexer = " << multiplexer << endl;
cout << "a * b = " << multiply_impure.Fetch();
cout << endl;
}
return 0;
}
从前面的代码片段中,我们看到在main()
函数中没有太多修改。我们修改的只是用于multiply_impure
变量的类类型,从Delay
改为Memoization
。然而,结果现在已经改变,因为我们将从multiply_impure()
函数的五次调用中获得完全相同的返回值。让我们看看以下截图来证明:
从前面的截图中,我们可以看到即使Multiplexer
的值增加了,计算的返回值始终相同。这是因为记录了第一次函数调用的返回值,所以不需要为剩余的调用再次运行函数。
正如我们在第二章中讨论的,在函数式编程中操作函数,在函数式编程中有一个不纯的函数似乎是错误的。将不纯的函数隐藏在记忆化后,如果代码确实需要不同的结果(非缓存结果),也可能会导致错误。明智地使用前述技术来缓存不纯的函数。
使用记忆化技术优化代码
记忆化对于应用于非纯函数或具有副作用的函数非常有用。然而,它也可以用于优化代码。通过使用记忆化,我们开发的代码将运行得更快。假设我们需要多次使用完全相同的函数和完全相同的传递参数运行。如果代码从我们记录值的地方获取值而不是运行函数,它将更快。对于昂贵的函数调用,使用记忆化也更好,因为我们不需要一遍又一遍地执行不必要的昂贵函数调用。
让我们创建一个代码来讨论进一步的优化。我们将使用Delay
类来演示与Memoization
类相比,它不是一个优化的代码。我们将有一个not_optimize_code.cpp
代码,它将使用Delay
类。在这个未优化的代码中,我们将调用我们在第四章中创建的fibonacci()
函数,使用递归算法重复方法调用。我们将把40
作为参数传递给fibonacci()
函数,并从fib40
类实例中调用Fetch()
方法五次。我们还将计算每次调用方法的经过时间,使用chrono
头文件中的high_resolution_clock
类记录开始和结束时间,通过用结束值减去开始值来获取经过时间。除了每个Fetch()
方法调用的经过时间,我们还计算整个代码的经过时间。not_optimize_code.cpp
代码的实现如下:
/* not_optimize_code.cpp */
#include <iostream>
#include <functional>
#include <chrono>
using namespace std;
template<class T> class Delay
{
private:
function<T()> m_func;
public:
Delay(function<T()> func): m_func(func)
{
}
T Fetch()
{
return m_func();
}
};
// Function for calculating Fibonacci sequence
int fibonacci(int n)
{
if(n <= 1)
return n;
return fibonacci(n-1) + fibonacci(n-2);
}
auto main() -> int
{
cout << "[not_optimize_code.cpp]" << endl;
// Recording start time for the program
auto start = chrono::high_resolution_clock::now();
// Initializing int variable to store the result
// from Fibonacci calculation
int fib40Result = 0;
// Constructing Delay<> named fib40
Delay<int> fib40([]()
{
return fibonacci(40);
});
for (int i = 1; i <= 5; ++i)
{
cout << "Invocation " << i << ". ";
// Recording start time
auto start = chrono::high_resolution_clock::now();
// Invoking the Fetch() method
// in fib40 instance
fib40Result = fib40.Fetch();
// Recording end time
auto finish = chrono::high_resolution_clock::now();
// Calculating the elapsed time
chrono::duration<double, milli> elapsed = finish - start;
// Displaying the result
cout << "Result = " << fib40Result << ". ";
// Displaying elapsed time
// for each fib40.Fetch() invocation
cout << "Consuming time = " << elapsed.count();
cout << " milliseconds" << endl;
}
// Recording end time for the program
auto finish = chrono::high_resolution_clock::now();
// Calculating the elapsed time for the program
chrono::duration<double, milli> elapsed = finish - start;
// Displaying elapsed time for the program
cout << "Total consuming time = ";
cout << elapsed.count() << " milliseconds" << endl;
return 0;
}
现在,让我们运行代码来获取前面代码处理的经过时间。以下截图是我们将在屏幕上看到的内容:
从前面的截图中,我们可以看到处理代码大约需要2357.79
毫秒。每次调用fib40.Fetch()
方法时,平均需要约470
毫秒,尽管我们将完全相同的参数传递给fibonacci()
函数,即40
。现在,让我们看看如果我们在前面的代码中使用记忆化技术会发生什么。我们不会修改代码太多,只是重构fib40
的实例化。现在它不再是从Delay
类实例化,而是从Memoization
类实例化。代码应该如下所示:
/* optimizing_memoization.cpp */
#include <iostream>
#include <functional>
#include <chrono>
using namespace std;
template<class T> class Memoization
{
private:
T const & (*m_subRoutine)(Memoization *);
mutable T m_recordedFunc;
function<T()> m_func;
static T const & ForceSubroutine(Memoization * d)
{
return d->DoRecording();
}
static T const & FetchSubroutine(Memoization * d)
{
return d->FetchRecording();
}
T const & FetchRecording()
{
return m_recordedFunc;
}
T const & DoRecording()
{
m_recordedFunc = m_func();
m_subRoutine = &FetchSubroutine;
return FetchRecording();
}
public:
Memoization(function<T()> func): m_func(func),
m_subRoutine(&ForceSubroutine),
m_recordedFunc(T())
{
}
T Fetch()
{
return m_subRoutine(this);
}
};
// Function for calculating Fibonacci sequence
int fibonacci(int n)
{
if(n <= 1)
return n;
return fibonacci(n-1) + fibonacci(n-2);
}
auto main() -> int
{
cout << "[optimizing_memoization.cpp]" << endl;
// Recording start time for the program
auto start = chrono::high_resolution_clock::now();
// Initializing int variable to store the result
// from Fibonacci calculation
int fib40Result = 0;
// Constructing Memoization<> named fib40
Memoization<int> fib40([]()
{
return fibonacci(40);
});
for (int i = 1; i <= 5; ++i)
{
cout << "Invocation " << i << ". ";
// Recording start time
auto start = chrono::high_resolution_clock::now();
// Invoking the Fetch() method
// in fib40 instance
fib40Result = fib40.Fetch();
// Recording end time
auto finish = chrono::high_resolution_clock::now();
// Calculating the elapsed time
chrono::duration<double, milli> elapsed = finish - start;
// Displaying the result
cout << "Result = " << fib40Result << ". ";
// Displaying elapsed time
// for each fib40.Fetch() invocation
cout << "Consuming time = " << elapsed.count();
cout << " milliseconds" << endl;
}
// Recording end time for the program
auto finish = chrono::high_resolution_clock::now();
// Calculating the elapsed time for the program
chrono::duration<double, milli> elapsed = finish - start;
// Displaying elapsed time for the program
cout << "Total consuming time = ";
cout << elapsed.count() << " milliseconds" << endl;
return 0;
}
当我们运行optimizing_memoization.cpp
代码时,我们将在控制台屏幕上看到以下内容:
令人惊讶的是,我们只需要494.681
毫秒来执行optimizing_memoization.cpp
代码。与not_optimize_code.cpp
代码相比,代码的速度大约快了4.7
倍。这是因为代码成功地缓存了将40
传递给其参数的fibonacci()
函数的结果。每次我们再次调用fib40.Fetch()
方法时,它将再次调用fibonacci()
函数,输入完全相同。代码将只返回缓存的结果,因此可以避免运行不必要的昂贵函数调用。
惰性评估的实际应用
在讨论了惰性评估的基本概念之后,让我们通过设计懒惰的方法来深入了解惰性评估。在本节中,我们将首先开发急切评估代码,然后将该代码重构为懒惰评估代码。我们开发的代码将生成一系列质数。首先,我们将使用for
循环迭代整数以获得急切评估中的质数。以下是我们所说的prime.cpp
代码:
/* prime.cpp */
#include <iostream>
#include <cmath>
using namespace std;
bool PrimeCheck(int i)
{
// All even numbers are not prime number
// except 2
if ((i % 2) == 0)
{
return i == 2;
}
// Calculating the square root of i
// and store in int data type variable
// if the argument i is not even number,
int sqr = sqrt(i);
// For numbers 9 and below,
// the prime numbers is simply the odd numbers
// For number above 9
// the prime numbers is all of odd numbers
// except the square number
for (int t = 3; t <= sqr; t += 2)
{
if (i % t == 0)
{
return false;
}
}
// The number 1 is not prime number
// but still passing the preceding test
return i != 1;
}
auto main() -> int
{
cout << "[delaying.cpp]" << endl;
// Initializing a counting variable
int n = 0;
// Displaying the first 100 prime numbers
cout << "List of the first 100 prime numbers:" << endl;
for (int i = 0; ; ++i)
{
if (PrimeCheck(i))
{
cout << i << "\t";
if (++n == 100)
return 0;
}
}
return 0;
}
在前面的代码中,我们有一个简单的PrimeCheck()
函数来分析整数是否是质数。之后,代码使用for
循环迭代无限整数,然后检查它是否是质数。如果我们得到了一百个质数,循环将结束。下面的截图是我们应该在控制台上看到的输出:
我们现在有一个使用急切评估生成质数的代码。如前面的截图所示,我们使用for
循环生成了一百个质数。接下来,我们将将其重构为懒惰代码。
设计 Chunk 和 Row 类
在prime.cpp
代码中,我们使用for
循环生成一行整数。在这一行中,有几个被称为Chunk的数字。现在,在重构代码之前,我们将为进一步讨论准备一个名为Row
和Chunk
的类。根据我们之前的类比,Row
类将保存整数序列,而Chunk
类将保存单个数字。我们将从数据中最小的部分开始,也就是 chunk。以下是Chunk
类的实现:
template<class T> class Chunk
{
private:
T m_value;
Row<T> m_lastRow;
public:
Chunk()
{
}
Chunk(T value, Row<T> lastRow): m_value(value),
m_lastRow(std::move(lastRow))
{
}
explicit Chunk(T value) : m_value(value)
{
}
T Value() const
{
return m_value;
}
Row<T> ShiftLastToFirst() const
{
return m_lastRow;
}
};
由于Row
类是由几个Chunk
类构成的,除了Chunk
本身的值之外,Chunk
类还具有当前Row
中Chunk
的下一个值,由m_lastRow
成员变量表示。我们还可以通过调用ShiftLastToFirst()
方法获取m_lastRow
的值。现在,让我们转到Row
类。该类的实现如下:
template<class T> class Row
{
private:
std::shared_ptr <Memoization<Chunk<T>>>
m_lazyChunk;
public:
Row()
{
}
explicit Row(T value)
{
auto chunk = ChunkPreparation<T>(value);
m_lazyChunk = std::make_shared<Memoization<Chunk<T>>>
(chunk);
}
Row(T value, Row row)
{
auto chunk = ChunkPreparation<T>( value, std::move(row));
m_lazyChunk = std::make_shared<Memoization<Chunk<T>>>(
chunk);
}
Row(std::function<Chunk<T>()> func): m_lazyChunk(
std::make_shared<Memoization<Chunk<T>>>(func))
{
}
bool IsEmpty() const
{
return !m_lazyChunk;
}
T Fetch() const
{
return m_lazyChunk->Fetch().Value();
}
Row<T> ShiftLastToFirst() const
{
return m_lazyChunk->Fetch().ShiftLastToFirst();
}
Row Pick(int n) const
{
if (n == 0 || IsEmpty())
return Row();
auto chunk = m_lazyChunk;
return Row([chunk, n]()
{
auto val = chunk->Fetch().Value();
auto row = chunk->Fetch().ShiftLastToFirst();
return Chunk<T>(val, row.Pick(n - 1));
});
}
};
如前面的代码片段所示,Row
类只有一个私有成员来存储Chunk
数据的记忆。Row
类有四个构造函数,我们将在下一段代码中全部使用。它还有Fetch()
方法,我们在上一节中设计Memoization
类时得到,用于获取m_lazyChunk
的值。其他方法对我们下一个懒惰的代码也很有用。IsEmpty()
方法将检查m_lazyChunk
的值是否为空,ShiftLastToFirst()
方法将获取m_lazyChunk
的最后一行,Pick(int n)
方法将取出我们稍后需要取出一百个整数质数的前n
行元素。
我们还可以看到Row
的一个构造函数调用了ChunkPreparation
类的构造函数。ChunkPreparation
类将使用给定的值和上一行的值初始化一个新的Chunk
类构造函数。该类的实现如下:
template<class T> class ChunkPreparation
{
public:
T m_value;
Row<T> m_row;
ChunkPreparation(T value, Row<T> row) :
m_value(value),
m_row(std::move(row))
{
}
explicit ChunkPreparation(T value) :
m_value(value)
{
}
Chunk<T> operator()()
{
return Chunk<T>(
m_value,
m_row);
}
};
如我们所见,通过调用operator()
,将使用给定的m_value
和m_row
值生成新的Chunk
。
连接几行
当我们计划生成一行质数时,我们必须能够将当前行与代码生成的新行连接起来。为了满足这个需求,以下是将连接两行的ConcatenateRows()
函数的实现:
template<class T> Row<T> ConcatenateRows(
Row<T> leftRow,
Row<T> rightRow)
{
if (leftRow.IsEmpty())
return rightRow;
return Row<T>([=]()
{
return Chunk<T>(
leftRow.Fetch(),
ConcatenateRows<T>(
leftRow.ShiftLastToFirst(),
rightRow));
});
}
当我们看一下前面的代码片段时,就可以清楚地知道ConcatenateRows()
函数的作用。如果leftRow
仍为空,只需返回第二行,即rightRow
。如果leftRow
和rightRow
都可用,我们可以返回已形成行的给定行的块。
迭代每个 Row 类的元素
在构建了质数行之后,我们需要迭代每行的元素进行操作,例如将值打印到控制台。为此,我们必须开发以下ForEach()
方法:
template<class T, class U> void ForEach( Row<T> row, U func)
{
while (!row.IsEmpty())
{
func(row.Fetch());
row = row.ShiftLastToFirst();
}
}
我们将把行本身和一个函数传递给ForEach()
方法。我们传递给它的函数将对行的每个元素运行。
为了方便我们在本章中开发惰性代码,我将把我们之前讨论的template
类捆绑到一个名为lazyevaluation.h
的单个头文件中;我们也可以在其他项目中重用它。头文件将包含Memoization
、Row
、Chunk
、ChunkPreparation
、ConcatenateRows
和ForEach
模板类。您可以自己创建头文件,也可以从 Packt 网站的代码库(github.com/PacktPublishing/LearningCPPFunctionalProgramming
)下载。
生成无限整数行
现在是时候生成无限整数行了,就像我们在之前的prime.cpp
代码中使用for
循环一样。但是,我们现在将创建一个名为GenerateInfiniteIntRow()
的新函数,以从几个整数块生成一个整数行。以下代码片段是该函数的实现:
Row<int> GenerateInfiniteIntRow( int initialNumber)
{
return Row<int>([initialNumber]()
{
return Chunk<int>(
initialNumber,
GenerateInfinityIntRow(
initialNumber + 1));
});
}
如我们所见,首先我们从initialNumber
创建Chunk
直到无穷大。这些块最终将转换为Row
数据类型。为了停止这个递归函数,我们可以在Row
类中调用Pick()
方法。
生成无限质数行
成功生成无限数字后,我们现在必须限制行只生成质数。我们将修改prime.cpp
代码中的CheckPrime()
函数。如果不是质数,我们将更改函数的返回值为Row<void*>(nullptr)
,如果相反,则为Row<void*>()
。函数的实现应该如下:
Row<void*> PrimeCheck(int i)
{
if ((i % 2) == 0)
{
if (i == 2)
return Row<void*>(nullptr);
else
return Row<void*>();
}
int sqr = sqrt(i);
for (int t = 3; t <= sqr; t = t + 2)
{
if (i % t == 0)
{
return Row<void*>();
}
}
if (i == 1)
return Row<void*>();
else
return Row<void*>(nullptr);
}
为什么我们需要改变函数的返回值?因为我们想将返回值传递给JoiningPrimeNumber()
函数,它将使用以下实现连接生成的 Chunk:
template<class T, class U>
auto JoiningPrimeNumber(
Row<T> row, U func) -> decltype(func())
{
return JoiningAllRows(
MappingRowByValue(row, func));
}
此外,MappingRowByValue()
函数将给定的行映射到给定的函数。函数的实现如下:
template<class T, class U>
auto MappingRowByValue(
Row<T> row, U func) -> Row<decltype(func())>
{
using V = decltype(func());
if (row.IsEmpty())
return Row<V>();
return Row<V>([row, func]()
{
return Chunk<V>(
func(),
MappingRowByValue(
row.ShiftLastToFirst(),
func));
});
}
成功使用JoiningPrimeNumber()
函数连接所有质数后,我们必须使用以下实现将其绑定到现有行使用Binding()
函数:
template<class T, class U> Row<T>
Binding( Row<T> row, U func)
{
return JoiningAllRows( MappingRow( row, func));
}
从前面的代码片段中,MappingRow()
函数将给定的行映射到给定的函数,然后JoiningAllRows()
将连接MappingRow()
的返回值中的所有行。MappingRow()
和JoiningAllRows()
函数的实现如下:
template<class T, class U>
auto MappingRow(
Row<T> row, U func) -> Row<decltype(
func(row.Fetch()))>
{
using V = decltype(func(row.Fetch()));
if (row.IsEmpty())
return Row<V>();
return Row<V>([row, func]()
{
return Chunk<V>(func(
row.Fetch()),
MappingRow(
row.ShiftLastToFirst(),
func));
});
}
template<class T> Row<T>
JoiningAllRows(
Row<Row<T>> rowOfRows)
{
while (!rowOfRows.IsEmpty() &&
rowOfRows.Fetch().IsEmpty())
{
rowOfRows = rowOfRows.ShiftLastToFirst();
}
if (rowOfRows.IsEmpty())
return Row<T>();
return Row<T>([rowOfRows]()
{
Row<T> row = rowOfRows.Fetch();
return Chunk<T>(
row.Fetch(),
ConcatenateRows(
row.ShiftLastToFirst(),
JoiningAllRows(
rowOfRows.ShiftLastToFirst())));
});
}
现在我们可以创建一个函数来限制无限整数行,实现如下:
Row<int> GenerateInfinitePrimeRow()
{
return Binding(
GenerateInfiniteIntRow(1),
[](int i)
{
return JoiningPrimeNumber(
PrimeCheck(i),
[i]()
{
return ConvertChunkToRow(i);
});
});
}
由于JoiningPrimeNumber()
函数的第二个参数需要一个行作为数据类型,我们需要使用以下实现使用ConvertChunkToRow()
函数将Chunk
转换为Row
:
template<class T> Row<T>
ConvertChunkToRow(
T value)
{
return Row<T>([value]()
{
return Chunk<T>(value);
});
}
现在我们可以使用所有前面的类和函数来重构我们的prime.cpp
代码。
重构急切评估为惰性评估
我们已经拥有了重构prime.cpp
代码为懒惰代码所需的所有函数。我们将创建一个prime_lazy.cpp
代码,首先生成无限整数,然后选择其中的前一百个元素。之后,我们迭代一百个元素,并将它们传递给将值打印到控制台的函数。代码应该如下所示:
/* prime_lazy.cpp */
#include <iostream>
#include <cmath>
#include "../lazyevaluation/lazyevaluation.h"
using namespace std;
Row<void*> PrimeCheck(int i)
{
// Use preceding implementation
}
Row<int> GenerateInfiniteIntRow(
int initialNumber)
{
// Use preceding implementation
}
template<class T, class U>
auto MappingRow(
Row<T> row, U func) -> Row<decltype(
func(row.Fetch()))>
{
// Use preceding implementation
}
template<class T, class U>
auto MappingRowByValue(
Row<T> row, U func) -> Row<decltype(func())>
{
// Use preceding implementation
}
template<class T> Row<T>
ConvertChunkToRow(
T value)
{
// Use preceding implementation
}
template<class T> Row<T>
JoiningAllRows(
Row<Row<T>> rowOfRows)
{
// Use preceding implementation
}
template<class T, class U> Row<T>
Binding(
Row<T> row, U func)
{
// Use preceding implementation
}
template<class T, class U>
auto JoiningPrimeNumber(
Row<T> row, U func) -> decltype(func())
{
// Use preceding implementation
}
Row<int> GenerateInfinitePrimeRow()
{
// Use preceding implementation
}
auto main() -> int
{
cout << "[prime_lazy.cpp]" << endl;
// Generating infinite prime numbers list
Row<int> r = GenerateInfinitePrimeRow();
// Picking the first 100 elements from preceding list
Row<int> firstAHundredPrimeNumbers = r.Pick(100);
// Displaying the first 100 prime numbers
cout << "List of the first 100 prime numbers:" << endl;
ForEach(
move(firstAHundredPrimeNumbers),
[](int const & i)
{
cout << i << "\t";
});
return 0;
}
从前面的代码中可以看出,我们有一个r
来保存无限的数字,然后我们选择了前一百个质数,并将它们存储到firstAHundredPrimeNumbers
中。为了将元素的值打印到控制台上,我们使用了ForEach()
函数,并将 Lambda 表达式传递给它。如果我们运行代码,结果与prime.cpp
代码完全相同,只是使用了不同的标题。如果我们运行prime_lazy.cpp
代码,我们应该在控制台上看到以下输出:
通过使用template
类,我们在本章中已经发现可以开发其他懒惰的代码来获得懒惰的好处。
在前面的prime_lazy.cpp
代码中,我省略了几行代码,这些代码是在前一节中编写的,以避免代码冗余。如果你发现由于代码不完整而难以跟踪代码,请转到github.com/PacktPublishing/LearningCPPFunctionalProgramming
。
总结
惰性评估不仅对函数式编程有用,而且对命令式编程也有好处。使用惰性评估,我们可以通过实现缓存和优化技术来拥有高效和更快的代码。
在下一章中,我们将讨论在函数式方法中可以使用的元编程。我们将讨论如何使用元编程来获得所有其好处,包括代码优化。
第六章:使用元编程优化代码
我们在上一章讨论了使用惰性评估的优化技术,并使用了延迟处理、缓存技术和记忆化来使我们的代码运行更快。在本章中,我们将使用元编程来优化代码,我们将创建一个将创建更多代码的代码。本章我们将讨论的主题如下:
-
元编程简介
-
构建模板元编程的部分
-
将流程控制重构为模板元编程
-
在编译时执行代码
-
模板元编程的优缺点
元编程简介
简单来说,元编程是一种通过使用代码来创建代码的技术。实现元编程时,我们编写一个计算机程序,操作其他程序并将它们视为数据。此外,模板是 C++中的一种编译时机制,它是图灵完备的,这意味着任何可以由计算机程序表达的计算都可以在运行时之前以某种形式通过模板元编程来计算。它还大量使用递归,并具有不可变变量。因此,在元编程中,我们创建的代码将在编译代码时运行。
使用宏预处理代码
要开始我们关于元编程的讨论,让我们回到 ANSI C 编程语言流行的时代。为了简单起见,我们使用了 C 预处理器创建了一个宏。C 参数化宏也被称为元函数,是元编程的一个例子。考虑以下参数化宏:
#define MAX(a,b) (((a) > (b)) ? (a) : (b))
由于 C++编程语言对 C 语言的兼容性有缺陷,我们可以使用 C++编译器编译前面的宏。让我们创建代码来使用前面的宏,代码如下:
/* macro.cpp */
#include <iostream>
using namespace std;
// Defining macro
#define MAX(a,b) (((a) > (b)) ? (a) : (b))
auto main() -> int
{
cout << "[macro.cpp]" << endl;
// Initializing two int variables
int x = 10;
int y = 20;
// Consuming the MAX macro
// and assign the result to z variable
int z = MAX(x,y);
// Displaying the result
cout << "Max number of " << x << " and " << y;
cout << " is " << z << endl;
return 0;
}
如前面的macro.cpp
代码所示,我们将两个参数传递给MAX
宏,因为它是一个参数化的宏,这意味着参数可以从用户那里获得。如果我们运行前面的代码,应该在控制台上看到以下输出:
正如我们在本章开头讨论的那样,元编程是在编译时运行的代码。通过在前面的代码中使用宏,我们可以展示从MAX
宏生成的新代码。预处理器将在编译时解析宏并带来新的代码。在编译时,编译器将修改代码如下:
auto main() -> int
{
// same code
// ...
int z = (((a) > (b)) ? (a) : (b)); // <-- Notice this section
// same code
// ...
return 0;
}
除了单行宏预处理器之外,我们还可以生成多行宏元函数。为了实现这一点,我们可以在每行末尾使用反斜杠字符。假设我们需要交换两个值。我们可以创建一个名为SWAP
的参数化宏,并像下面的代码一样使用它:
/* macroswap.cpp */
#include <iostream>
using namespace std;
// Defining multi line macro
#define SWAP(a,b) { \
(a) ^= (b); \
(b) ^= (a); \
(a) ^= (b); \
}
auto main() -> int
{
cout << "[macroswap.cpp]" << endl;
// Initializing two int variables
int x = 10;
int y = 20;
// Displaying original variable value
cout << "before swapping" << endl;
cout << "x = " << x << ", y = " << y ;
cout << endl << endl;
// Consuming the SWAP macro
SWAP(x,y);
// Displaying swapped variable value
cout << "after swapping" << endl;
cout << "x = " << x << ", y = " << y;
cout << endl;
return 0;
}
如前面的代码所示,我们将创建一个多行预处理器宏,并在每行末尾使用反斜杠字符。每次调用SWAP
参数化宏时,它将被替换为宏的实现。如果我们运行前面的代码,将在控制台上看到以下输出:
现在我们对元编程有了基本的了解,特别是在元函数中,我们可以在下一个主题中进一步学习。
在每个宏预处理器的实现中,我们为每个变量使用括号,因为预处理器只是用宏的实现替换我们的代码。假设我们有以下宏:
MULTIPLY(a,b) (a * b)
如果我们将数字作为参数传递,那么这不会成为问题。然而,如果我们将一个操作作为参数传递,就会出现问题。例如,如果我们像下面这样使用MULTIPLY
宏:
MULTIPLY(x+2,y+5);
然后编译器将其替换为(x+2*y+5)
。这是因为宏只是用x + 2
表达式替换a
变量,用y + 5
表达式替换b
变量,而没有额外的括号。因为乘法的顺序高于加法,我们将得到以下结果:
(x+2y+5)
这并不是我们期望的结果。因此,最好的方法是在参数的每个变量中使用括号。
解剖标准库中的模板元编程
我们在第一章中讨论了标准库,深入现代 C++,并在上一章中也处理了它。C++语言中提供的标准库主要是一个包含不完整函数的模板。然而,它将用于生成完整的函数。模板元编程是 C++模板,用于在编译时生成 C++类型和代码。
让我们挑选标准库中的一个类--Array
类。在Array
类中,我们可以为其定义一个数据类型。当我们实例化数组时,编译器实际上会生成我们定义的数据类型的数组的代码。现在,让我们尝试构建一个简单的Array
模板实现,如下所示:
template<typename T>
class Array
{
T element;
};
然后,我们实例化char
和int
数组如下:
Array<char> arrChar;
Array<int> arrInt;
编译器所做的是基于我们定义的数据类型创建这两个模板的实现。虽然我们在代码中看不到这一点,但编译器实际上创建了以下代码:
class ArrayChar
{
char element;
};
class ArrayInt
{
int element;
};
ArrayChar arrChar;
ArrayInt arrInt;
正如我们在前面的代码片段中所看到的,模板元编程是在编译时创建另一个代码的代码。
构建模板元编程
在进一步讨论模板元编程之前,最好讨论一下构建模板元编程的骨架。有四个因素构成模板元编程--类型,值,分支和递归。在这个话题中,我们将深入探讨构成模板的因素。
在模板中添加一个值到变量
在本章的开头,我们讨论了元函数的概念,当我们谈到宏预处理器时。在宏预处理器中,我们明确地操纵源代码;在这种情况下,宏(元函数)操纵源代码。相反,在 C++模板元编程中,我们使用类型。这意味着元函数是一个与类型一起工作的函数。因此,使用模板元编程的更好方法是尽可能只使用类型参数。当我们谈论模板元编程中的变量时,实际上它并不是一个变量,因为它上面的值是不能被修改的。我们需要的是变量的名称,这样我们才能访问它。因为我们将使用类型编码,命名的值是typedef
,正如我们在以下代码片段中所看到的:
struct ValueDataType
{
typedef int valueDataType;
};
通过使用前面的代码,我们将int
类型存储到valueDataType
别名中,这样我们就可以使用valueDataType
变量来访问数据类型。如果我们需要将值而不是数据类型存储到变量中,我们可以使用enum
,这样它将成为enum
本身的数据成员。如果我们想要存储值,让我们看一下以下代码片段:
struct ValuePlaceHolder
{
enum
{
value = 1
};
};
基于前面的代码片段,我们现在可以访问value
变量以获取其值。
将函数映射到输入参数
我们可以将变量添加到模板元编程中。现在,我们接下来要做的是检索用户参数并将它们映射到一个函数。假设我们想要开发一个Multiplexer
函数,它将两个值相乘,我们必须使用模板元编程。以下代码片段可用于解决这个问题:
template<int A, int B>
struct Multiplexer
{
enum
{
result = A * B
};
};
正如我们在前面的代码片段中所看到的,模板需要用户提供两个参数A
和B
,它将使用它们来通过将这两个参数相乘来获取result
变量的值。我们可以使用以下代码访问结果变量:
int i = Multiplexer<2, 3>::result;
如果我们运行前面的代码片段,i
变量将存储6
,因为它将计算2
乘以3
。
根据条件选择正确的过程
当我们有多个函数时,我们必须根据某些条件选择其中一个。我们可以通过提供template
类的两个替代特化来构建条件分支,如下所示:
template<typename A, typename B>
struct CheckingType
{
enum
{
result = 0
};
};
template<typename X>
struct CheckingType<X, X>
{
enum
{
result = 1
};
};
正如我们在前面的template
代码中所看到的,我们有两个模板,它们的类型分别为X
和A
/B
。当模板只有一个类型,即typename X
时,这意味着我们比较的两种类型(CheckingType <X, X>
)完全相同。否则,这两种数据类型是不同的。以下代码片段可以用来使用前面的两个模板:
if (CheckingType<UnknownType, int>::result)
{
// run the function if the UnknownType is int
}
else
{
// otherwise run any function
}
正如我们在前面的代码片段中所看到的,我们试图将UnknownType
数据类型与int
类型进行比较。UnknownType
数据类型可能来自其他过程。然后,我们可以通过使用模板来比较这两种类型来决定我们想要运行的下一个过程。
到目前为止,你可能会想知道模板多编程如何帮助我们进行代码优化。很快我们将使用模板元编程来优化代码。然而,我们需要讨论其他事情来巩固我们在模板多编程中的知识。现在,请耐心阅读。
递归重复这个过程
我们已经成功地将值和数据类型添加到模板中,然后根据当前条件创建了一个分支来决定下一个过程。在基本模板中,我们还需要考虑重复这个过程。然而,由于模板中的变量是不可变的,我们无法迭代序列。相反,我们必须像我们在第四章中讨论的那样,通过递归算法重复这个过程。
假设我们正在开发一个模板来计算阶乘值。我们首先要做的是开发一个将I
值传递给函数的通用模板,如下所示:
template <int I>
struct Factorial
{
enum
{
value = I * Factorial<I-1>::value
};
};
正如我们在前面的代码中所看到的,我们可以通过运行以下代码来获得阶乘的值:
Factorial<I>::value;
在前面的代码中,I
是一个整数。
接下来,我们必须开发一个模板来确保它不会陷入无限循环。我们可以创建以下模板,将零(0
)作为参数传递给它:
template <>
struct Factorial<0>
{
enum
{
value = 1
};
};
现在我们有一对模板,可以在编译时生成阶乘的值。以下是一个示例代码,用于在编译时获取Factorial(10)
的值:
int main()
{
int fact10 = Factorial<10>::value;
}
如果我们运行前面的代码,我们将得到10
的阶乘的结果3628800
。
在编译时选择类型
正如我们在前面的主题中讨论的,type
是模板的基本部分。然而,我们可以根据用户的输入选择特定的类型。让我们创建一个模板,可以决定变量中应该使用什么类型。以下的types.cpp
代码将展示模板的实现:
/* types.cpp */
#include <iostream>
using namespace std;
// Defining a data type
// in template
template<typename T>
struct datatype
{
using type = T;
};
auto main() -> int
{
cout << "[types.cpp]" << endl;
// Selecting a data type in compile time
using t = typename datatype<int>::type;
// Using the selected data type
t myVar = 123;
// Displaying the selected data type
cout << "myVar = " << myVar;
return 0;
}
正如我们在前面的代码中所看到的,我们有一个名为datatype
的模板。这个模板可以用来选择我们传递给它的type
。我们可以使用using
关键字将一个变量分配给type
。从前面的types.cpp
代码中,我们将把一个变量t
分配给datatype
模板中的type
。现在,t
变量将是int
,因为我们将int
数据类型传递给了模板。
我们还可以创建一个代码来根据当前条件选择正确的数据类型。我们将有一个IfElseDataType
模板,它接受三个参数,即predicate
,当predicate
参数为 true 时的数据类型,以及当predicate
参数为 false 时的数据类型。代码将如下所示:
/* selectingtype.cpp */
#include <iostream>
using namespace std;
// Defining IfElseDataType template
template<
bool predicate,
typename TrueType,
typename FalseType>
struct IfElseDataType
{
};
// Defining template for TRUE condition
// passed to 'predicate' parameter
template<
typename TrueType,
typename FalseType>
struct IfElseDataType<
true,
TrueType,
FalseType>
{
typedef TrueType type;
};
// Defining template for FALSE condition
// passed to 'predicate' parameter
template<
typename TrueType,
typename FalseType>
struct IfElseDataType<
false,
TrueType,
FalseType>
{
typedef FalseType type;
};
auto main() -> int
{
cout << "[types.cpp]" << endl;
// Consuming template and passing
// 'SHRT_MAX == 2147483647'
// It will be FALSE
// since the maximum value of short
// is 32767
// so the data type for myVar
// will be 'int'
IfElseDataType<
SHRT_MAX == 2147483647,
short,
int>::type myVar;
// Assigning myVar to maximum value
// of 'short' type
myVar = 2147483647;
// Displaying the data type of myVar
cout << "myVar has type ";
cout << typeid(myVar).name() << endl;
return 0;
}
现在,通过IfElseDataType
模板,我们可以根据我们的条件选择正确的类型给变量。假设我们想要将2147483647
赋给一个变量,以便我们可以检查它是否是一个短数字。如果是,myVar
将是short
类型,否则将是int
类型。此外,由于short
类型的最大值是32767
,通过给定谓词为SHRT_MAX == 2147483647
将导致FALSE
。因此,myVar
的类型将是int
类型,如我们将在控制台上看到的以下输出:
使用模板元编程进行流程控制
代码流是编写程序的重要方面。在许多编程语言中,它们有if-else
,switch
和do-while
语句来安排代码的流程。现在,让我们将通常的代码流重构为基于模板的流程。我们将首先使用if-else
语句,然后是switch
语句,最后以模板的形式结束do-while
语句。
根据当前条件决定下一个过程
现在是时候使用我们之前讨论过的模板了。假设我们有两个函数,我们必须根据某个条件进行选择。我们通常会使用if-else
语句,如下所示:
/* condition.cpp */
#include <iostream>
using namespace std;
// Function that will run
// if the condition is TRUE
void TrueStatement()
{
cout << "True Statement is run." << endl;
}
// Function that will run
// if the condition is FALSE
void FalseStatement()
{
cout << "False Statement is run." << endl;
}
auto main() -> int
{
cout << "[condition.cpp]" << endl;
// Choosing the function
// based on the condition
if (2 + 3 == 5)
TrueStatement();
else
FalseStatement();
return 0;
}
正如我们在前面的代码中所看到的,我们有两个函数--TrueStatement()
和FalseStatement()
。我们在代码中还有一个条件--2 + 3 == 5
。由于条件是TRUE
,因此TrueStatement()
函数将被运行,如我们在下面的截图中所看到的:
现在,让我们重构前面的condition.cpp
代码。我们将在这里创建三个模板。首先,输入条件的初始化模板如下:
template<bool predicate> class IfElse
然后,我们为每个条件创建两个模板--TRUE
或FALSE
。名称将如下:
template<> class IfElse<true>
template<> class IfElse<false>
前面代码片段中的每个模板将运行我们之前创建的函数--TrueStatement()
和FalseStatement()
函数。我们将得到完整的代码,如下所示的conditionmeta.cpp
代码:
/* conditionmeta.cpp */
#include <iostream>
using namespace std;
// Function that will run
// if the condition is TRUE
void TrueStatement()
{
cout << "True Statement is run." << endl;
}
// Function that will run
// if the condition is FALSE
void FalseStatement()
{
cout << "False Statement is run." << endl;
}
// Defining IfElse template
template<bool predicate>
class IfElse
{
};
// Defining template for TRUE condition
// passed to 'predicate' parameter
template<>
class IfElse<true>
{
public:
static inline void func()
{
TrueStatement();
}
};
// Defining template for FALSE condition
// passed to 'predicate' parameter
template<>
class IfElse<false>
{
public:
static inline void func()
{
FalseStatement();
}
};
auto main() -> int
{
cout << "[conditionmeta.cpp]" << endl;
// Consuming IfElse template
IfElse<(2 + 3 == 5)>::func();
return 0;
}
正如我们所看到的,我们将条件放在IfElse
模板的括号中,然后在模板内调用func()
方法。如果我们运行conditionmeta.cpp
代码,我们将得到与condition.cpp
代码完全相同的输出,如下所示:
现在我们有了if-else
语句来流动我们的模板元编程代码。
选择正确的语句
在 C++编程中,以及其他编程语言中,我们使用switch
语句根据我们给switch
语句的值来选择某个过程。如果值与 switch case 中的一个匹配,它将运行该 case 下的过程。让我们看一下下面的switch.cpp
代码,它实现了switch
语句:
/* switch.cpp */
#include <iostream>
using namespace std;
// Function to find out
// the square of an int
int Square(int a)
{
return a * a;
}
auto main() -> int
{
cout << "[switch.cpp]" << endl;
// Initializing two int variables
int input = 2;
int output = 0;
// Passing the correct argument
// to the function
switch (input)
{
case 1:
output = Square(1);
break;
case 2:
output = Square(2);
break;
default:
output = Square(0);
break;
}
// Displaying the result
cout << "The result is " << output << endl;
return 0;
}
正如我们在前面的代码中所看到的,我们有一个名为Square()
的函数,它接受一个参数。我们传递给它的参数是基于我们给 switch 语句的值。由于我们传递给 switch 的值是2
,Square(2)
方法将被运行。下面的截图是我们将在控制台屏幕上看到的内容:
要将switch.cpp
代码重构为模板元编程,我们必须创建三个包含我们计划运行的函数的模板。首先,我们将创建初始化模板以从用户那里检索值,如下所示:
template<int val> class SwitchTemplate
前面的初始化模板也将用于默认值。接下来,我们将为每个可能的值添加两个模板,如下所示:
template<> class SwitchTemplate<1>
template<> class SwitchTemplate<2>
每个前面的模板将运行Square()
函数并根据模板的值传递参数。完整的代码如下所示:
/* switchmeta.cpp */
#include <iostream>
using namespace std;
// Function to find out
// the square of an int
int Square(int a)
{
return a * a;
}
// Defining template for
// default output
// for any input value
template<int val>
class SwitchTemplate
{
public:
static inline int func()
{
return Square(0);
}
};
// Defining template for
// specific input value
// 'val' = 1
template<>
class SwitchTemplate<1>
{
public:
static inline int func()
{
return Square(1);
}
};
// Defining template for
// specific input value
// 'val' = 2
template<>
class SwitchTemplate<2>
{
public:
static inline int func()
{
return Square(2);
}
};
auto main() -> int
{
cout << "[switchmeta.cpp]" << endl;
// Defining a constant variable
const int i = 2;
// Consuming the SwitchTemplate template
int output = SwitchTemplate<i>::func();
// Displaying the result
cout << "The result is " << output << endl;
return 0;
}
如我们所见,我们与conditionmeta.cpp
做的一样--我们调用模板内的func()
方法来运行所选的函数。此switch-case
条件的值是我们放在尖括号中的模板。如果我们运行前面的switchmeta.cpp
代码,我们将在控制台上看到以下输出:
如前面的截图所示,与switch.cpp
代码相比,我们对switchmeta.cpp
代码得到了完全相同的输出。因此,我们已成功将switch.cpp
代码重构为模板元编程。
循环该过程
当我们迭代某些内容时,通常使用do-while
循环。假设我们需要打印某些数字,直到达到零(0
)。代码如下所示:
/* loop.cpp */
#include <iostream>
using namespace std;
// Function for printing
// given number
void PrintNumber(int i)
{
cout << i << "\t";
}
auto main() -> int
{
cout << "[loop.cpp]" << endl;
// Initializing an int variable
// marking as maximum number
int i = 100;
// Looping to print out
// the numbers below i variable
cout << "List of numbers between 100 and 1";
cout << endl;
do
{
PrintNumber(i);
}
while (--i > 0);
cout << endl;
return 0;
}
如前面的代码所示,我们将打印数字100
,减少其值,并再次打印。它将一直运行,直到数字达到零(0
)。控制台上的输出应该如下所示:
现在,让我们将其重构为模板元编程。在这里,我们只需要两个模板来实现模板元编程中的do-while
循环。首先,我们将创建以下模板:
template<int limit> class DoWhile
前面代码中的限制是传递给do-while
循环的值。为了不使循环变成无限循环,当它达到零(0
)时,我们必须设计DoWhile
模板,如下所示:
template<> class DoWhile<0>
前面的模板将什么也不做,因为它只用于中断循环。对do-while
循环的完全重构如下loopmeta.cpp
代码:
/* loopmeta.cpp */
#include <iostream>
using namespace std;
// Function for printing
// given number
void PrintNumber(int i)
{
cout << i << "\t";
}
// Defining template for printing number
// passing to its 'limit' parameter
// It's only run
// if the 'limit' has not been reached
template<int limit>
class DoWhile
{
private:
enum
{
run = (limit-1) != 0
};
public:
static inline void func()
{
PrintNumber(limit);
DoWhile<run == true ? (limit-1) : 0>
::func();
}
};
// Defining template for doing nothing
// when the 'limit' reaches 0
template<>
class DoWhile<0>
{
public:
static inline void func()
{
}
};
auto main() -> int
{
cout << "[loopmeta.cpp]" << endl;
// Defining a constant variable
const int i = 100;
// Looping to print out
// the numbers below i variable
// by consuming the DoWhile
cout << "List of numbers between 100 and 1";
cout << endl;
DoWhile<i>::func();
cout << endl;
return 0;
}
然后我们调用模板内的func()
方法来运行我们想要的函数。如果我们运行代码,我们将在屏幕上看到以下输出:
同样,我们已成功将loop.cpp
代码重构为loopmeta.cpp
代码,因为两者的输出完全相同。
在编译时执行代码
正如我们之前讨论的,模板元编程将通过创建新代码在编译时运行代码。现在,让我们看看如何获取编译时常量并在本节生成编译时类。
获取编译时常量
为了检索编译时常量,让我们创建一个包含斐波那契算法模板的代码。我们将使用模板,这样编译器将在编译时提供值。代码应该如下所示:
/* fibonaccimeta.cpp */
#include <iostream>
using namespace std;
// Defining Fibonacci template
// to calculate the Fibonacci sequence
template <int number>
struct Fibonacci
{
enum
{
value =
Fibonacci<number - 1>::value +
Fibonacci<number - 2>::value
};
};
// Defining template for
// specific input value
// 'number' = 1
template <>
struct Fibonacci<1>
{
enum
{
value = 1
};
};
// Defining template for
// specific input value
// 'number' = 0
template <>
struct Fibonacci<0>
{
enum
{
value = 0
};
};
auto main() -> int
{
cout << "[fibonaccimeta.cpp]" << endl;
// Displaying the compile-time constant
cout << "Getting compile-time constant:";
cout << endl;
cout << "Fibonacci(25) = ";
cout << Fibonacci<25>::value;
cout << endl;
return 0;
}
如前面的代码所示,斐波那契模板中的值变量将提供编译时常量。如果我们运行前面的代码,我们将在控制台屏幕上看到以下输出:
现在,我们有75025
,这是由编译器生成的编译时常量。
使用编译时类生成生成类
除了生成编译时常量之外,我们还将在编译时生成类。假设我们有一个模板来找出范围为0
到X
的质数。以下的isprimemeta.cpp
代码将解释模板元编程的实现以找到质数:
/* isprimemeta.cpp */
#include <iostream>
using namespace std;
// Defining template that decide
// whether or not the passed argument
// is a prime number
template <
int lastNumber,
int secondLastNumber>
class IsPrime
{
public:
enum
{
primeNumber = (
(lastNumber % secondLastNumber) &&
IsPrime<lastNumber, secondLastNumber - 1>
::primeNumber)
};
};
// Defining template for checking
// the number passed to the 'number' parameter
// is a prime number
template <int number>
class IsPrime<number, 1>
{
public:
enum
{
primeNumber = 1
};
};
// Defining template to print out
// the passed argument is it's a prime number
template <int number>
class PrimeNumberPrinter
{
public:
PrimeNumberPrinter<number - 1> printer;
enum
{
primeNumber = IsPrime<number, number - 1>
::primeNumber
};
void func()
{
printer.func();
if (primeNumber)
{
cout << number << "\t";
}
}
};
// Defining template to just ignoring the number
// we pass 1 as argument to the parameter
// since 1 is not prime number
template<>
class PrimeNumberPrinter<1>
{
public:
enum
{
primeNumber = 0
};
void func()
{
}
};
int main()
{
cout << "[isprimemeta.cpp]" << endl;
// Displaying the prime numbers between 1 and 500
cout << "Filtering the numbers between 1 and 500 ";
cout << "for of the prime numbers:" << endl;
// Consuming PrimeNumberPrinter template
PrimeNumberPrinter<500> printer;
// invoking func() method from the template
printer.func();
cout << endl;
return 0;
}
有两种不同角色的模板--质数检查器,确保传递的数字是质数,以及打印机,将质数显示到控制台。当代码访问PrimeNumberPrinter<500> printer
和printer.func()
时,编译器将在编译时生成类。当我们运行前面的isprimemeta.cpp
代码时,我们将在控制台屏幕上看到以下输出:
由于我们将500
传递给模板,我们将从0
到500
得到质数。前面的输出证明了编译器成功生成了一个编译时类,因此我们可以得到正确的值。
元编程的利与弊
在我们讨论完模板元编程之后,以下是我们得到的优点:
-
模板元编程没有副作用,因为它是不可变的,所以我们不能修改现有类型
-
与不实现元编程的代码相比,代码可读性更好
-
它减少了代码的重复
尽管我们可以从模板元编程中获得好处,但也有一些缺点,如下所示:
-
语法相当复杂。
-
编译时间较长,因为现在我们在编译时执行代码。
-
编译器可以更好地优化生成的代码并执行内联,例如 C 中的
qsort()
函数和 C++中的sort
模板。在 C 中,qsort()
函数接受一个指向比较函数的指针,因此将有一个未内联的qsort
代码副本。它将通过指针调用比较例程。在 C++中,std::sort
是一个模板,它可以接受一个functor
对象作为比较器。对于每种不同类型用作比较器,都有一个不同的std::sort
副本。如果我们使用一个具有重载的operator()
函数的functor
类,比较器的调用可以轻松地内联到std::sort
的这个副本中。
总结
元编程,特别是模板元编程,可以自动为我们创建新的代码,这样我们就不需要在源代码中编写大量的代码。通过使用模板元编程,我们可以重构代码的流程控制,并在编译时执行代码。
在下一章中,我们将讨论并发技术,这将为我们构建的应用程序带来响应性增强。我们可以使用并行技术同时运行代码中的进程。
第七章:使用并发运行并行执行
在前一章中,我们讨论了模板元编程,它将使代码在编译时执行。它还将改善我们的代码流程控制,因为我们可以使用模板重构流程。现在,在本章中,我们将讨论 C++中的并发,当我们同时运行两个或更多个进程时,我们必须再次控制流程。在本章中,我们将讨论以下主题:
-
在 C++编程中运行单个和多个线程
-
同步线程以避免死锁
-
在 Windows 中使用handle资源创建线程
C++中的并发
许多编程语言今天都提供了对并发的支持。在并发编程中,代码的计算在重叠的时间段内执行,而不是顺序执行。这将使我们的程序响应迅速,因为代码不需要等待所有计算完成。假设我们想开发一个可以同时播放视频和下载大型视频文件的程序。如果没有并发技术,我们必须等待视频成功下载后才能播放另一个视频文件。通过使用这种技术,我们可以分割这两个任务,播放和下载视频,然后同时并发运行它们。
在 C++11 宣布之前,C++程序员依赖于Boost::thread
来使用多线程技术创建并发程序。在多线程中,我们将进程分解为最小的序列,并同时运行这些小进程。现在,在 C++11 库中,我们得到了thread
类来满足我们使用多线程技术的并发需求。
处理单线程代码
要使用thread
类,我们只需要创建一个std::thread
的实例,并将函数名作为参数传递。然后我们调用std::join()
来暂停进程,直到所选线程完成其进程。让我们看一下以下singlethread.cpp
的代码:
/* singlethread.cpp */
#include <thread>
#include <iostream>
using namespace std;
void threadProc()
{
cout << "Thread ID: ";
cout << this_thread::get_id() << endl;
}
auto main() -> int
{
cout << "[singlethread.cpp]" << endl;
thread thread1(threadProc);
thread1.join();
return 0;
}
正如我们在前面的代码中所看到的,我们有一个名为threadProc()
的函数,并将其传递给main()
函数中的thread1
初始化。初始化后,我们调用join()
方法来执行thread1
对象。我们在控制台上看到的输出应该如下:
我们已经成功地在我们的代码中运行了一个线程。现在,让我们在main()
函数中添加一行代码,来迭代一行代码。我们将同时并发运行它们。singlethread2.cpp
的代码如下:
/* singlethread2.cpp */
#include <thread>
#include <chrono>
#include <iostream>
using namespace std;
void threadProc()
{
for (int i = 0; i < 5; i++)
{
cout << "thread: current i = ";
cout << i << endl;
}
}
auto main() -> int
{
cout << "[singlethread2.cpp]" << endl;
thread thread1(threadProc);
for (int i = 0; i < 5; i++)
{
cout << "main : current i = " << i << endl;
this_thread::sleep_for(
chrono::milliseconds(5)); }
thread1.join();
return 0;
}
正如我们在前面的代码中所看到的,我们添加了一个for
循环来迭代一些代码,并与thread1
同时运行。为了理解它,我们也在threadProc()
函数中添加了一个for
循环。让我们看一下以下截图,以弄清楚我们将得到什么输出:
我们看到threadProc()
函数和main()
函数中的代码同时并发运行。你们可能会得到不同的结果,但没关系,因为结果是无法预测的,这取决于设备本身。然而,目前我们已经能够同时运行两个进程。
我多次运行了前面的代码,以获得我们在前面截图中看到的输出。你可能会看到threadProc()
和main()
函数之间的不同顺序,或者得到混乱的输出,因为线程的流程是不可预测的。
处理多线程代码
在多线程技术中,我们同时运行两个或更多个线程。假设我们同时运行五个线程。我们可以使用以下multithread.cpp
代码,将这五个线程存储在一个数组中:
/* multithread.cpp */
#include <thread>
#include <iostream>
using namespace std;
void threadProc()
{
cout << "Thread ID: ";
cout << this_thread::get_id() << endl;
}
auto main() -> int
{
cout << "[multithread.cpp]" << endl;
thread threads[5];
for (int i = 0; i < 5; ++i)
{
threads[i] = thread(threadProc);
}
for (auto& thread : threads)
{
thread.join();
}
return 0;
}
在我们根据前面的代码初始化这五个线程之后,我们将运行join()
方法来执行所有线程。通过使用join()
方法,程序将等待调用线程中的所有进程完成,然后继续下一个进程(如果有的话)。我们在控制台中看到的结果如下:
在前面的截图中,我们看到所有五个线程都已成功执行。我们也可以使用 Lambda 表达式来初始化线程。下面的lambdathread.cpp
代码是从前面使用 Lambda 而不是创建一个单独的函数进行重构的代码:
/* lambdathread.cpp */
#include <thread>
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[lambdathread.cpp]" << endl;
thread threads[5];
for (int i = 0; i < 5; ++i)
{
threads[i] = thread([]()
{
cout << "Thread ID: ";
cout << this_thread::get_id() << endl;
});
}
for (auto& thread : threads)
{
thread.join();
}
return 0;
}
如果我们看lambdathread.cpp
代码与multithread.cpp
代码,没有什么显著的变化。然而,由于该函数只会被调用一次,最好使用 Lambda,这样更容易维护。我们在控制台上看到的输出如下截图所示,与multithread.cpp
代码的输出相比并没有太大的不同:
尽管在运行lambdathread.cpp
与multithread.cpp
代码进行比较时我们得到了相同的输出,但是当我们使用 Lambda 表达式初始化线程时,我们有一个清晰的代码。我们不需要创建另一个方法传递给Thread
,例如threadProc()
,因为这个方法实际上只使用一次。
再次注意,您在屏幕上看到的结果可能与我给出的截图不同。
使用互斥锁同步线程
到目前为止,我们已经成功地执行了一个多线程代码。然而,如果我们在线程内部使用一个共享对象并对其进行操作,就会出现问题。这被称为同步。在本节中,我们将尝试通过应用mutex
技术来避免这个问题。
避免同步问题
正如我们之前讨论的,在这一部分,我们必须确保在线程中运行的共享对象在执行时给出正确的值。假设我们有一个名为counter
的全局变量,并且我们计划在我们拥有的所有五个线程中增加它的值。每个线程将执行10000
次增量迭代,因此我们期望得到所有五个线程的结果为50000
。代码如下:
/* notsync.cpp */
#include <thread>
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[notsync.cpp]" << endl;
int counter = 0;
thread threads[5];
for (int i = 0; i < 5; ++i)
{
threads[i] = thread([&counter]()
{
for (int i = 0; i < 10000; ++i)
{
++counter;
cout << "Thread ID: ";
cout << this_thread::get_id();
cout << "\tCurrent Counter = ";
cout << counter << endl;
}
});
}
for (auto& thread : threads)
{
thread.join();
}
cout << "Final result = " << counter << endl;
return 0;
}
现在,让我们看一下当我们运行前面的代码时,我们可能在控制台上看到的以下截图:
不幸的是,根据前面的截图,我们没有得到我们期望的结果。这是因为增量过程不是一个原子操作,原子操作将保证并发进程的隔离。
如果您得到了不同的输出,不要担心,我们仍然在正确的轨道上,因为这个程序展示了同步问题,接下来您将看到。
如果我们深入追踪输出,我们会看到有两个线程执行counter
变量的完全相同的值,正如我们在下面的截图中所看到的:
我们看到 ID 为2504
和5524
的线程在counter
变量的值为44143
时访问了该变量。这就是当我们运行前面的代码时为什么会得到意外的结果。现在我们需要使增量操作成为一个原子操作,这样就可以在操作期间不允许其他进程读取或更改被读取或更改的状态。
为了解决这个问题,我们可以使用mutex
类来使我们的计数器变量线程安全
。这意味着在线程访问计数器变量之前,它必须确保该变量不被其他线程访问。我们可以使用mutex
类中的lock()
和unlock()
方法来锁定和解锁目标变量。让我们看一下下面的mutex.cpp
代码来演示mutex
的实现:
/* mutex.cpp */
#include <thread>
#include <mutex>
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[mutex.cpp]" << endl;
mutex mtx;
int counter = 0;
thread threads[5];
for (int i = 0; i < 5; ++i)
{
threads[i] = thread([&counter, &mtx]()
{
for (int i = 0; i < 10000; ++i)
{
mtx.lock();
++counter;
mtx.unlock();
cout << "Thread ID: ";
cout << this_thread::get_id();
cout << "\tCurrent Counter = ";
cout << counter << endl;
}
});
}
for (auto& thread : threads)
{
thread.join();
}
cout << "Final result = " << counter << endl;
return 0;
}
在前面的代码中,我们可以看到,在代码递增counter
变量之前,它调用了lock()
方法。之后,它调用unlock()
方法来通知其他线程,counter
变量现在可以自由操作。如果我们运行前面的代码,应该在控制台上看到以下输出:
通过使用mutex
类,现在我们得到了我们期望的结果,如前面的截图所示。
自动解锁变量
现在我们知道如何锁定变量,以确保没有两个线程同时处理相同的值。然而,如果在线程调用unlock()
方法之前抛出异常,问题就会发生。如果变量的状态保持锁定,程序将完全被锁定。为了解决这个问题,我们可以使用lock_guard<mutex>
来锁定变量,并确保无论发生什么情况,它都将在作用域结束时解锁。以下代码片段是通过添加lock_guard<mutex>
功能从前面的代码重构而来的:
/* automutex.cpp */
#include <thread>
#include <mutex>
#include <iostream>
using namespace std;
auto main() -> int
{
cout << "[automutex.cpp]" << endl;
mutex mtx;
int counter = 0;
thread threads[5];
for (int i = 0; i < 5; ++i)
{
threads[i] = thread([&counter, &mtx]()
{
for (int i = 0; i < 10000; ++i)
{
{
lock_guard <mutex> guard(mtx);
++counter;
}
cout << "Thread ID: ";
cout << this_thread::get_id();
cout << "\tCurrent Counter = ";
cout << counter << endl;
}
});
}
for (auto& thread : threads)
{
thread.join();
}
cout << "Final result = " << counter << endl;
return 0;
}
从前面的automutex.cpp
代码中可以看出,在递增counter
变量之前,它调用了lock_guard <mutex> guard(mtx)
。如果我们运行代码,我们将得到与mutex.cpp
代码完全相同的输出。然而,现在我们有一个不会不可预测地被锁定的程序。
使用递归互斥量避免死锁
在前一节中,我们使用lock_guard
来确保变量不被多个线程访问。然而,如果多个lock_guard
获取锁,我们仍然会面临问题。在下面的代码片段中,我们有两个函数将调用lock_guard
--Multiplexer()
和Divisor()
。除此之外,我们还有一个函数将调用这两个函数--RunAll()
,它将在调用这两个函数之前先调用lock_guard
。代码应该如下所示:
/* deadlock.cpp */
#include <thread>
#include <mutex>
#include <iostream>
using namespace std;
struct Math
{
mutex mtx;
int m_content;
Math() : m_content(0)
{
}
// This method will lock the mutex
void Multiplexer(int i)
{
lock_guard<mutex> lock(mtx);
m_content *= i;
cout << "Multiplexer() is called. m_content = ";
cout << m_content << endl;
}
// This method will lock the mutex also
void Divisor(int i)
{
lock_guard<mutex> lock(mtx);
m_content /= i;
cout << "Divisor() is called. m_content = ";
cout << m_content << endl;
}
// This method will invoke
// the two preceding methods
// which each method locks the mutex
void RunAll(int a)
{
lock_guard<mutex> lock(mtx);
Multiplexer(a);
Divisor(a);
}
};
auto main() -> int
{
cout << "[deadlock.cpp]" << endl;
// Instantiating Math struct
// and invoking the RunAll() method
Math math;
math.RunAll(10);
return 0;
}
我们将成功编译以下代码片段。然而,如果我们运行前面的代码,由于死锁,程序将无法退出。这是因为同一个互斥量不能被多个线程两次获取。当调用RunAll()
函数时,它会获取lock
对象。RunAll()
函数内部的Multiplexer()
函数也想要获取lock
。然而,lock
已经被RunAll()
函数锁定。为了解决这个问题,我们可以将lock_guard<mutex>
替换为lock_guard<recursive_mutex>
,如下面的代码片段所示:
/* recursivemutex.cpp */
#include <thread>
#include <mutex>
#include <iostream>
using namespace std;
struct Math
{
recursive_mutex mtx;
int m_content;
Math() : m_content(1)
{
}
// This method will lock the mutex
void Multiplexer(int i)
{
lock_guard<recursive_mutex> lock(mtx);
m_content *= i;
cout << "Multiplexer() is called. m_content = ";
cout << m_content << endl;
}
// This method will lock the mutex also
void Divisor(int i)
{
lock_guard<recursive_mutex> lock(mtx);
m_content /= i;
cout << "Divisor() is called. m_content = ";
cout << m_content << endl;
}
// This method will invoke
// the two preceding methods
// which each method locks the mutex
void RunAll(int a)
{
lock_guard<recursive_mutex> lock(mtx);
Multiplexer(a);
Divisor(a);
}
};
auto main() -> int
{
cout << "[recursivemutex.cpp]" << endl;
// Instantiating Math struct
// and invoking the RunAll() method
Math math;
math.RunAll(10);
return 0;
}
现在,我们可以成功编译和运行前面的代码。我们可以使用lock_guard<recursive_mutex>
类,它允许多次锁定互斥量而不会陷入死锁。当我们运行前面的代码时,控制台上将看到以下截图:
现在,我们知道如果我们想要调用递归锁定相同的mutex
的函数,我们需要使用一个递归mutex
。
了解 Windows 操作系统中的线程处理
让我们转向一个被许多用户计算机广泛使用的特定操作系统,那就是 Windows。我们的代码必须在来自领先操作系统供应商的商业平台上运行,比如微软。因此,我们现在将在 Windows 操作系统中运行线程。在这个操作系统中,线程是一个内核资源,这意味着它是由操作系统内核创建和拥有的对象,并且存在于内核中。内核本身是一个核心程序,对系统中的一切都有完全控制。在本节中,我们将在 Windows 操作系统中开发一个线程,以便我们的程序可以在这个操作系统中正常工作。
使用句柄处理
在 Windows 操作系统中,句柄是对资源的抽象引用值。在本讨论中,我们将使用抽象引用来持有线程。假设我们有一个threadProc()
函数,将在hnd
变量中持有的线程中调用。代码如下:
/* threadhandle.cpp */
#include <iostream>
#include <windows.h>
using namespace std;
auto threadProc(void*) -> unsigned long
{
cout << "threadProc() is run." << endl;
return 100;
}
auto main() -> int
{
cout << "[threadhandle.cpp]" << endl;
auto hnd = HANDLE
{
CreateThread(
nullptr,
0,
threadProc,
nullptr,
0,
nullptr)
};
if (hnd)
{
WaitForSingleObject(hnd, INFINITE);
unsigned long exitCode;
GetExitCodeThread(hnd, &exitCode);
cout << "The result = " << exitCode << endl;
CloseHandle(hnd);
}
return 0;
}
如前所述,我们使用windows.h
头文件提供的CreateThread()
函数生成线程。目前,我们只传递nullptr
值作为默认参数,除了threadProc
作为我们将从线程中调用的函数。
在我们初始化线程的句柄之后,我们可以确保hnd
变量包含线程的句柄,然后调用WaitForSingleObject()
函数。这类似于我们在前面一节中使用的join()
方法,它将运行线程并等待直到线程完成。由于线程句柄是我们使用的资源,请不要忘记使用CloseHandle()
函数释放它。如果我们运行上述代码,我们将在控制台屏幕上看到以下输出:
正如我们所看到的,我们成功地运行了线程,因为我们从threadProc()
函数中得到了预期的进程。
重构为唯一句柄
现在,为了简化我们的编程过程,我们将创建一个名为NullHandle
的类,它将在我们不再需要它时自动释放资源。它将从我们也将开发的UniqueHandle
类构造而来。这些类可以在uniquehandle.h
文件中找到。UniqueHandle
的实现如下:
template <typename C>
class UniqueHandle
{
private:
HANDLE m_val;
void Close()
{
if (*this)
{
C::Exit(m_val);
}
}
public:
// Copy assignment operator
UniqueHandle(UniqueHandle const &) = delete;
auto operator=(UniqueHandle const &)->UniqueHandle & = delete;
// UniqueHandle constructor
explicit UniqueHandle(HANDLE value = C::Invalid()) :
m_val{ value }
{
}
// Move assignment operator
UniqueHandle(UniqueHandle && other) :
m_val{ other.Release() }
{
}
// Move assignment operator
auto operator=(UniqueHandle && other) -> UniqueHandle &
{
if (this != &other)
{
Reset(other.Release());
}
return *this;
}
// Destructor of UniqueHandle class
~UniqueHandle()
{
Close();
}
// bool operator for equality
explicit operator bool() const
{
return m_val != C::Invalid();
}
// Method for retrieving the HANDLE value
HANDLE Get() const
{
return m_val;
}
// Method for releasing the HANDLE value
HANDLE Release()
{
auto value = m_val;
m_val = C::Invalid();
return value;
}
// Method for reseting the HANDLE
bool Reset(HANDLE value = C::Invalid())
{
if (m_val != value)
{
Close();
m_val = value;
}
return static_cast<bool>(*this);
}
};
如我们所见,我们有一个完整的UniqueHandle
类实现,可以被实例化,并且将在其析构函数中自动关闭句柄。要使用NullHandle
对象,我们将使用以下代码:
using NullHandle = UniqueHandle<NullHandleCharacteristics>;
NullHandleCharacteristics
结构的实现如下:
struct NullHandleCharacteristics
{
// Returning nullptr when the HANDLE is invalid
static HANDLE Invalid()
{
return nullptr;
}
// Exit the HANDLE by closing it
static void Exit(HANDLE val)
{
CloseHandle(val);
}
};
现在,让我们重构之前的threadhandle.cpp
代码。我们将用NullHandle
替换HANDLE
,代码如下:
auto hnd = NullHandle
{
CreateThread(
nullptr,
0,
threadProc,
nullptr,
0,
nullptr)
};
然后,我们将创建一个名为WaitOneThread()
的新函数来调用线程本身,并等待直到它完成。实现应该如下:
auto WaitOneThread(
HANDLE const h,
DWORD const ms = INFINITE) -> bool
{
auto const r = WaitForSingleObject(
h,
ms);
// Inform that thread is not idle
if (r == WAIT_OBJECT_0)
return true;
// Inform that thread is not idle
if (r == WAIT_TIMEOUT)
return false;
throw WinException();
}
通过使用WaitOneThread()
函数,我们可以知道线程是否已经运行。WinException
结构可以实现如下:
struct WinException
{
unsigned long error;
explicit WinException(
unsigned long value = GetLastError()) :
error{ value }
{
}
};
现在,在我们初始化hnd
HANDLE 之后,我们可以添加以下代码片段到main()
函数中:
if (hnd)
{
if (WaitOneThread(hnd.Get(), 0))
cout << "Before running thread" << endl;
WaitOneThread(hnd.Get());
if (WaitOneThread(hnd.Get(), 0))
cout << "After running thread" << endl;
unsigned long exitCode;
GetExitCodeThread(hnd.Get(), &exitCode);
cout << "The result = " << exitCode << endl;
}
从上述代码中可以看出,我们调用WaitOneThread()
函数,并将0
作为ms
参数传递给WaitForSingleObject()
函数调用,以了解其状态。我们可以将INFINITE
值传递给它,以调用线程并等待直到它完成。以下是从threadhandle.cpp
代码重构而来并使用了UniqueHandle
类的threaduniquehandle.cpp
代码:
/* threaduniquehandle.cpp */
#include <iostream>
#include <windows.h>
#include "../uniquehandle_h/uniquehandle.h"
using namespace std;
unsigned long threadProc(void*)
{
cout << "threadProc() is run." << endl;
return 100;
}
struct WinException
{
unsigned long error;
explicit WinException(
unsigned long value = GetLastError()) :
error{ value }
{
}
};
auto WaitOneThread(
HANDLE const h,
DWORD const ms = INFINITE) -> bool
{
auto const r = WaitForSingleObject(
h,
ms);
// Inform that thread is not idle
if (r == WAIT_OBJECT_0)
return true;
// Inform that thread is not idle
if (r == WAIT_TIMEOUT)
return false;
throw WinException();
}
auto main() -> int
{
cout << "[threaduniquehandle.cpp]" << endl;
auto hnd = NullHandle
{
CreateThread(
nullptr,
0,
threadProc,
nullptr,
0,
nullptr)
};
if (hnd)
{
if (WaitOneThread(hnd.Get(), 0))
cout << "Before running thread" << endl;
WaitOneThread(hnd.Get());
if (WaitOneThread(hnd.Get(), 0))
cout << "After running thread" << endl;
unsigned long exitCode;
GetExitCodeThread(hnd.Get(), &exitCode);
cout << "The result = " << exitCode << endl;
}
return 0;
}
以下截图是我们应该在控制台屏幕上看到的输出:
从上述截图中可以看出,我们没有Before running thread
行。这是因为每次未调用线程时,我们将得到WAIT_TIMEOUT
输出。而且,我们成功执行了threadProc()
函数中的代码。
触发事件
在 Windows 中玩耍线程之后,让我们尝试另一种并发类型--Event
。它是系统可以触发的动作。要进一步了解它,让我们看一下以下代码片段,其中我们创建了一个名为Event
的新类,它实现了UniqueHandle
:
class Event
{
private:
NullHandle hnd;
public:
Event(Event const &) = delete;
auto operator=(Event const &)->Event & = delete;
~Event() = default;
explicit Event(bool manual) :
hnd
{
CreateEvent(nullptr,
manual, false, nullptr)
}
{
if (!hnd)
throw WinException();
}
explicit Event(EventType evType) :
hnd
{
CreateEvent(
nullptr,
static_cast<BOOL>(evType),
false,
nullptr)
}
{
if (!hnd)
throw WinException();
}
Event(Event && other) throw() :
hnd
{
other.hnd.Release()
}
{
}
auto operator=(Event && other) throw()->Event &
{
hnd = move(other.hnd);
}
void Set()
{
cout << "The event is set" << endl;
SetEvent(hnd.Get());
}
void Clear()
{
cout << "The event is cleared" << endl;
ResetEvent(hnd.Get());
}
auto Wait(
DWORD const ms = INFINITE) -> bool
{
auto const result = WaitForSingleObject(
hnd.Get(), ms);
return result == WAIT_OBJECT_0;
}
};
如我们在上述Event
类实现中所看到的,我们有Set()
、Clear()
和Wait()
方法来分别设置事件、清除事件和等待事件完成。我们有两种事件类型,即自动重置和手动重置,声明如下:
enum class EventType
{
AutoReset,
ManualReset
};
现在,我们将在main()
函数中创建内容。我们首先实例化Event
类,然后检查事件信号。如果没有被标记,我们将设置事件。相反,我们将清除事件。代码将是下面的event.cpp
代码:
/* event.cpp */
#include <iostream>
#include <windows.h>
#include "../uniquehandle_h/uniquehandle.h"
using namespace std;
struct WinException
{
unsigned long error;
explicit WinException(
unsigned long value = GetLastError()) :
error{ value }
{
}
};
enum class EventType
{
AutoReset,
ManualReset
};
class Event
{
private:
NullHandle hnd;
public:
Event(Event const &) = delete;
auto operator=(Event const &)->Event & = delete;
~Event() = default;
explicit Event(bool manual) :
hnd
{
CreateEvent(nullptr,
manual, false, nullptr)
}
{
if (!hnd)
throw WinException();
}
explicit Event(EventType evType) :
hnd
{
CreateEvent(
nullptr,
static_cast<BOOL>(evType),
false,
nullptr)
}
{
if (!hnd)
throw WinException();
}
Event(Event && other) throw() :
hnd
{
other.hnd.Release()
}
{
}
auto operator=(Event && other) throw() -> Event &
{
hnd = move(other.hnd);
}
void Set()
{
cout << "The event is set" << endl;
SetEvent(hnd.Get());
}
void Clear()
{
cout << "The event is cleared" << endl;
ResetEvent(hnd.Get());
}
auto Wait(
DWORD const ms = INFINITE) -> bool
{
auto const result = WaitForSingleObject(
hnd.Get(), ms);
return result == WAIT_OBJECT_0;
}
};
void CheckEventSignaling( bool b)
{
if (b)
{
cout << "The event is signaled" << endl;
}
else
{
cout << "The event is not signaled" << endl;
}
}
auto main() -> int
{
cout << "[event.cpp]" << endl;
auto ev = Event{
EventType::ManualReset };
CheckEventSignaling(ev.Wait(0));
ev.Set();
CheckEventSignaling(ev.Wait(0));
ev.Clear();
CheckEventSignaling(ev.Wait(0));
return 0;
}
正如我们在前面的代码中所看到的,这是代码的作用:
-
它在
main()
函数中创建了Event
类的实例,并手动重置了事件。 -
它调用
CheckEventSignaling()
函数,通过将Wait()
函数传递给CheckEventSignaling()
函数来找出事件的状态,然后调用WaitForSingleObject()
函数。 -
它调用了
Set()
和Reset()
函数。 -
现在运行前面的
event.cpp
代码。您将在控制台上看到以下输出:
如果我们看一下前面的截图,首先Event
类的初始化没有被标记。然后我们设置了事件,现在它被标记为CheckEventSignaling()
方法的状态。在这里,我们可以通过调用WaitForSingleObject()
函数来检查标记事件的状态。
从线程调用事件
现在,让我们使用线程调用Event
类。但在此之前,我们必须能够包装多个线程,一起调用它们,并等待它们的进程完成。以下代码块是将打包线程的Wrap()
函数:
void Wrap(HANDLE *)
{
}
template <typename T, typename... Args>
void Wrap(
HANDLE * left,
T const & right,
Args const & ... args)
{
*left = right.Get();
Wrap(++left, args...);
}
当我们加入所有线程时,我们将调用前面的Wrap()
函数。因此,我们将需要另一个名为WaitAllThreads()
的函数,正如我们在下面的代码片段中所看到的:
template <typename... Args>
void WaitAllThreads(Args const & ... args)
{
HANDLE handles[sizeof...(Args)];
Wrap(handles, args...);
WaitForMultipleObjects(
sizeof...(Args),
handles,
true,
INFINITE);
}
现在,我们可以创建我们的完整代码,将使用以下eventthread.cpp
代码运行两个线程:
/* eventthread.cpp */
#include <iostream>
#include <windows.h>
#include "../uniquehandle_h/uniquehandle.h"
using namespace std;
void Wrap(HANDLE *)
{
}
template <typename T, typename... Args>
void Wrap(
HANDLE * left,
T const & right,
Args const & ... args)
{
*left = right.Get();
Wrap(++left, args...);
}
template <typename... Args>
void WaitAllThreads(Args const & ... args)
{
HANDLE handles[sizeof...(Args)];
Wrap(handles, args...);
WaitForMultipleObjects(
sizeof...(Args),
handles,
true,
INFINITE);
}
auto threadProc(void*) -> unsigned long
{
cout << "Thread ID: ";
cout << GetCurrentThreadId() << endl;
return 120;
}
auto main() -> int
{
cout << "[eventthread.cpp]" << endl;
auto thread1 = NullHandle
{
CreateThread(
nullptr,
0,
threadProc,
nullptr,
CREATE_SUSPENDED,
nullptr)
};
auto thread2 = NullHandle
{
CreateThread(
nullptr,
0,
threadProc,
nullptr,
CREATE_SUSPENDED,
nullptr)
};
ResumeThread(thread1.Get());
ResumeThread(thread2.Get());
WaitAllThreads(thread1, thread2);
return 0;
}
此外,如果我们运行前面的eventthread.cpp
代码,我们将在控制台屏幕上看到以下输出:
我们已成功触发了一个Event
,因此它可以被设置为标记,并且可以在event.cpp
代码中被清除为未标记。我们还成功地包装了多个线程,然后在eventthread.cpp
代码中一起调用它们。现在,让我们将这两个代码连接起来,这样我们就可以从线程中访问事件。代码应该像下面的eventthread2.cpp
代码一样:
/* eventthread2.cpp */
#include <iostream>
#include <windows.h>
#include "../uniquehandle_h/uniquehandle.h"
using namespace std;
struct WinException
{
unsigned long error;
explicit WinException(
unsigned long value = GetLastError()) :
error{ value }
{
}
};
enum class EventType
{
AutoReset,
ManualReset
};
class Event
{
private:
NullHandle hnd;
public:
Event(Event const &) = delete;
auto operator=(Event const &)->Event & = delete;
~Event() = default;
explicit Event(bool manual) :
hnd
{
CreateEvent(nullptr,
manual, false, nullptr)
}
{
if (!hnd)
throw WinException();
}
explicit Event(EventType evType) :
hnd
{
CreateEvent(
nullptr,
static_cast<BOOL>(evType),
false,
nullptr)
}
{
if (!hnd)
throw WinException();
}
Event(Event && other) throw() :
hnd
{
other.hnd.Release()
}
{
}
auto operator=(Event && other) throw() -> Event &
{
hnd = move(other.hnd);
}
void Set()
{
cout << "The event is set" << endl;
SetEvent(hnd.Get());
}
void Clear()
{
cout << "The event is cleared" << endl;
ResetEvent(hnd.Get());
}
auto Wait( DWORD const ms = INFINITE) -> bool
{
auto const result = WaitForSingleObject(
hnd.Get(), ms);
return result == WAIT_OBJECT_0;
}
};
void Wrap(HANDLE *)
{
}
template <typename T, typename... Args>
void Wrap(
HANDLE * left,
T const & right,
Args const & ... args)
{
*left = right.Get();
Wrap(++left, args...);
}
template <typename... Args>
void WaitAllThreads(Args const & ... args)
{
HANDLE handles[sizeof...(Args)];
Wrap(handles, args...);
WaitForMultipleObjects(
sizeof...(Args),
handles,
true,
INFINITE);
}
static auto ev = Event{
EventType::ManualReset };
auto threadProc(void*) -> unsigned long
{
cout << "Thread ID: ";
cout << GetCurrentThreadId() << endl;
ev.Wait();
cout << "Run Thread ID: ";
cout << GetCurrentThreadId() << endl;
return 120;
}
auto main() -> int
{
cout << "[eventthread2.cpp]" << endl;
auto thread1 = NullHandle
{
CreateThread(
nullptr,
0,
threadProc,
nullptr,
0,
nullptr)
};
auto thread2 = NullHandle
{
CreateThread(
nullptr,
0,
threadProc,
nullptr,
0,
nullptr)
};
Sleep(100);
ev.Set();
Sleep(100);
WaitAllThreads(thread1, thread2);
return 0;
}
在前面的eventthread2.cpp
代码中,我们尝试使用线程触发事件。首先我们初始化了两个NullHandle
对象线程。然后,我们设置了事件,并调用Sleep()
函数使事件激活。然后WaitAllThreads()
函数调用threadProc()
函数并运行每个线程。这将通过调用ev.Wait()
函数来触发事件。然后线程将运行。以下截图是我们将在控制台屏幕上看到的输出:
前面的代码是我们手动设置为重置事件的事件。这意味着我们必须说明何时清除事件。现在,我们将AutoReset
传递给事件实例。我们还将稍微修改threadProc()
函数。我们正在谈论的是以下eventthread3.cpp
代码:
/* eventthread3.cpp */
#include <iostream>
#include <windows.h>
#include "../uniquehandle_h/uniquehandle.h"
using namespace std;
struct WinException
{
unsigned long error;
explicit WinException(
unsigned long value = GetLastError()) :
error{ value }
{
}
};
enum class EventType
{
AutoReset,
ManualReset
};
class Event
{
private:
NullHandle hnd;
public:
Event(Event const &) = delete;
auto operator=(Event const &)->Event & = delete;
~Event() = default;
explicit Event(bool manual) :
hnd
{
CreateEvent(nullptr,
manual, false, nullptr)
}
{
if (!hnd)
throw WinException();
}
explicit Event(EventType evType) :
hnd
{
CreateEvent(
nullptr,
static_cast<BOOL>(evType),
false,
nullptr)
}
{
if (!hnd)
throw WinException();
}
Event(Event && other) throw() :
hnd
{
other.hnd.Release()
}
{
}
auto operator=(Event && other) throw() -> Event &
{
hnd = move(other.hnd);
}
void Set()
{
cout << "The event is set" << endl;
SetEvent(hnd.Get());
}
void Clear()
{
cout << "The event is cleared" << endl;
ResetEvent(hnd.Get());
}
auto Wait(
DWORD const ms = INFINITE) -> bool
{
auto const result = WaitForSingleObject(
hnd.Get(), ms);
return result == WAIT_OBJECT_0;
}
};
void Wrap(HANDLE *)
{
}
template <typename T, typename... Args>
void Wrap(
HANDLE * left,
T const & right,
Args const & ... args)
{
*left = right.Get();
Wrap(++left, args...);
}
template <typename... Args>
void WaitAllThreads(Args const & ... args)
{
HANDLE handles[sizeof...(Args)];
Wrap(handles, args...);
WaitForMultipleObjects(
sizeof...(Args),
handles,
true,
INFINITE);
}
static auto ev = Event{
EventType::AutoReset };
auto threadProc(void*) -> unsigned long
{
cout << "Thread ID: ";
cout << GetCurrentThreadId() << endl;
ev.Wait();
cout << "Run Thread ID: ";
cout << GetCurrentThreadId() << endl;
Sleep(1000);
ev.Set();
return 120;
}
auto main() -> int
{
cout << "[eventthread3.cpp]" << endl;
auto thread1 = NullHandle
{
CreateThread(
nullptr,
0,
threadProc,
nullptr,
0,
nullptr)
};
auto thread2 = NullHandle
{
CreateThread(
nullptr,
0,
threadProc,
nullptr,
0,
nullptr)
};
Sleep(100);
ev.Set();
Sleep(100);
WaitAllThreads(thread1, thread2);
return 0;
}
正如我们在前面的代码中所看到的,我们将事件的Set()
方法从main()
函数移动到threadProc()
函数中。现在,每次调用threadProc()
函数时,事件都会自动设置。以下截图是我们应该在控制台屏幕上看到的输出:
总结
在本章中,我们学习了 C++并发的概念。我们现在可以处理单个线程以及多线程。我们还可以同步多线程,使其可以平稳运行;因此,我们可以避免同步问题和死锁。最后,我们可以使用 Windows 中的句柄资源来创建线程,并使用该事件触发事件。
在下一章中,我们将运用前几章学到的知识以函数式的方式来制作一个应用程序。它还将解释如何测试使用 C++语言构建的应用程序。
第八章:在函数式方法中创建和调试应用程序
我们在前几章讨论了一些基本的技术,以函数式编程的方式开发,包括一等函数、纯函数和不可变对象。在本章中,我们将使用我们在前几章中学到的所有技术,以函数式的方式产生一个应用程序。它还将解释如何调试使用 C++语言构建的应用程序。
在本章中,我们将涵盖以下主题:
-
准备一个命令式代码作为基本代码,以便转换为函数式代码
-
实现纯函数到基本代码
-
实现模板元编程到基本代码
-
使用 Lambda 表达式实现基本代码的过滤技术
-
实现递归技术到基本代码
-
实现备忘录技术到基本代码
-
调试代码以解决,如果我们得到了意外的结果
准备一个命令式类
我们现在将开发功能类,这样我们就可以将其用于我们的功能程序。在那之前,让我们准备一个名为Customer
的新的命令式类。该类将有一个名为id
的int
属性,作为唯一的客户 ID 号。它还有四个字符串属性,用于存储关于我们客户的信息--name
、address
、phoneNumber
和email
。该类还有一个标志--isActive
--用于指示我们的客户是否活跃。如果客户与我们签订了合同,他们被视为活跃客户。另一个属性是registeredCustomers
,用于保存我们所有已注册的客户,无论是否活跃。我们将使registeredCustomers
成员变为static
,这样我们就可以从类外部填充它,并且可以保持Customer
类的列表。
除了这些属性,我们的类还将有四个方法来访问我们属性的列表。它们将是以下方法:
-
GetActiveCustomerNames()
: 这可以用来获取活跃客户的名称列表 -
GetActiveCustomerAddresses()
: 这可以用来获取活跃客户的地址列表 -
GetActiveCustomerPhoneNumbers()
: 这可以用来获取活跃客户的电话号码列表 -
GetActiveCustomerEmails()
: 这可以用来获取活跃客户的电子邮件列表
现在,让我们看一下我们可以在Step01
文件夹中找到的以下Customer.h
代码,以适应我们之前的情景:
/* Customer.h - Step01 */
#ifndef __CUSTOMER_H__
#define __CUSTOMER_H__
#include <string>
#include <vector>
class Customer
{
public:
static std::vector<Customer> registeredCustomers;
int id = 0;
std::string name;
std::string address;
std::string phoneNumber;
std::string email;
bool isActive = true;
std::vector<std::string> GetActiveCustomerNames();
std::vector<std::string> GetActiveCustomerAddresses();
std::vector<std::string> GetActiveCustomerPhoneNumbers();
std::vector<std::string> GetActiveCustomerEmails();
};
#endif // __CUSTOMER_H__
从前面的代码中,我们有四个尚未定义的公共方法。现在,让我们定义它们,就像我们在以下Customer.cpp
代码中所看到的那样:
/* Customer.cpp - Step01 */
#include "Customer.h"
using namespace std;
vector<Customer> Customer::registeredCustomers;
vector<string> Customer::GetActiveCustomerNames()
{
vector<string> returnList;
for (auto &customer : Customer::registeredCustomers)
{
if (customer.isActive)
{
returnList.push_back(customer.name);
}
}
return returnList;
}
vector<string> Customer::GetActiveCustomerAddresses()
{
vector<string> returnList;
for (auto &customer : Customer::registeredCustomers)
{
if (customer.isActive)
{
returnList.push_back(customer.address);
}
}
return returnList;
}
vector<string> Customer::GetActiveCustomerPhoneNumbers()
{
vector<string> returnList;
for (auto &customer : Customer::registeredCustomers)
{
if (customer.isActive)
{
returnList.push_back(customer.phoneNumber);
}
}
return returnList;
}
vector<string> Customer::GetActiveCustomerEmails()
{
vector<string> returnList;
for (auto &customer : Customer::registeredCustomers)
{
if (customer.isActive)
{
returnList.push_back(customer.email);
}
}
return returnList;
}
从前面的代码中,我们可以看到Customer
类中我们有四个方法的定义。例如,在GetActiveCustomerNames()
方法中,代码循环遍历registeredCustomers
向量中的每个元素,以找出活跃客户。如果找到了,代码将提取每个客户的名称并将其存储到returnList
向量中。方法完成后,方法将把returnList
的结果提供给方法用户。
现在,让我们使用以下main.cpp
代码来使用前面的类:
/* Main.cpp - Step01 */
#include <iostream>
#include <algorithm>
#include "Customer.h"
using namespace std;
void RegisterCustomers()
{
int i = 0;
bool b = false;
// Initialize name
vector<string> nameList =
{
"William",
"Aiden",
"Rowan",
"Jamie",
"Quinn",
"Haiden",
"Logan",
"Emerson",
"Sherlyn",
"Molly"
};
// Clear the registeredCustomers vector array
Customer::registeredCustomers.clear();
for (auto name : nameList)
{
// Create Customer object
// and fill all properties
Customer c;
c.id = i++;
c.name = name;
c.address = "somewhere";
c.phoneNumber = "0123";
c.email = name + "@xyz.com";
c.isActive = b;
// Flip the b value
b = !b;
// Send data to the registeredCustomers
Customer::registeredCustomers.push_back(c);
}
}
auto main() -> int
{
cout << "[Step01]" << endl;
cout << "--------" << endl;
// Fill the Customer::registeredCustomers
// with the content
RegisterCustomers();
// Instance Customer object
Customer customer;
// Get the active customer names
cout << "List of active customer names:" << endl;
vector<string> activeCustomerNames =
customer.GetActiveCustomerNames();
for (auto &name : activeCustomerNames)
{
cout << name << endl;
}
return 0;
}
从前面的代码中,在main()
方法中,我们可以看到我们首先从RegisterCustomers()
方法中注册我们的客户。在那里,我们用一堆我们的客户信息填充了Customer
类的静态公共属性registeredCustomers
。之后,代码实例化了Customer
类,并调用了类的名为GetActiveCustomerNames()
的方法。正如我们所看到的,该方法返回一个包含我们将在activeCustomerNames
向量中存储的活跃客户名称列表的字符串向量。现在,我们可以迭代向量以提取活跃客户名称的列表。以下是我们应该在控制台中看到的输出:
正如我们在RegisterCustomer()
方法中所看到的,十个客户中只有五个是活跃的,所以并不是所有的名字都会在前面的输出中列出。我们可以尝试剩下的三种方法来获取关于活跃客户的信息,特别是他们的地址、电话号码和电子邮件地址。本章的目标是利用我们在前几章学到的概念,使用函数式方法来制作一个应用程序。所以,让我们看看我们如何能够实现这一点。
重构命令式类为函数式类
实际上,前面的Customer
类可以很好地工作,我们已经成功地调用了它的方法。然而,这个类仍然可以通过将其转换为一个函数式类来进行调整。正如我们在前面的代码中所看到的,我们可以实现一个纯函数、一等函数、高阶函数和记忆化,使其变得函数式。因此,在本节中,我们将重构Customer
类,使其成为一个函数式类,并使用我们从前几章学到的知识。在接下来的部分,我们将实现我们在前一章中讨论过的函数式方法,即一等函数。
将函数作为参数传递
正如我们在第二章中讨论的函数式编程中的函数操作,我们可以重写函数成为一等函数,这意味着我们可以将一个函数传递给另一个函数。我们将简化Step01
代码中所有四个方法的定义,然后通过将其传递给另一个名为GetActiveCustomerByFunctionField()
的方法来调用该函数。我们还将创建一个名为GetActiveCustomerByField()
的新方法来选择我们应该运行的正确方法。Customer
类的定义现在如下Customer.h
代码所示:
/* Customer.h - Step02 */
#ifndef __CUSTOMER_H__
#define __CUSTOMER_H__
#include <string>
#include <vector>
#include <functional>
class Customer
{
private:
std::string GetActiveCustomerNames(
Customer customer) const;
std::string GetActiveCustomerAddresses(
Customer customer) const;
std::string GetActiveCustomerPhoneNumbers(
Customer customer) const;
std::string GetActiveCustomerEmails(
Customer customer) const;
public:
static std::vector<Customer> registeredCustomers;
int id = 0;
std::string name;
std::string address;
std::string phoneNumber;
std::string email;
bool isActive = true;
std::vector<std::string> GetActiveCustomerByField(
const std::string &field);
std::vector<std::string> GetActiveCustomerByFunctionField(
std::function<std::string(const Customer&, Customer)>
funcField);
};
#endif //#ifndef __CUSTOMER_H__
正如我们在前面的头文件中所看到的,除了四个私有方法之外,我们还添加了一个名为GetActiveCustomerByFunctionField()
的新公共方法,当我们需要一个属性列表时,我们将调用它。现在,让我们定义前面头文件中创建的四个方法。代码应该如下Customer.cpp
文件所示:
/* Customer.cpp - Step02 */
#include <stdexcept>
#include "Customer.h"
using namespace std;
vector<Customer> Customer::registeredCustomers;
string Customer::GetActiveCustomerNames(
Customer customer) const
{
return customer.name;
}
string Customer::GetActiveCustomerAddresses(
Customer customer) const
{
return customer.address;
}
string Customer::GetActiveCustomerPhoneNumbers(
Customer customer) const
{
return customer.phoneNumber;
}
string Customer::GetActiveCustomerEmails(
Customer customer) const
{
return customer.email;
}
vector<string> Customer::GetActiveCustomerByFunctionField(
function<string(const Customer&, Customer)> funcField)
{
vector<string> returnList;
Customer c;
for (auto customer : Customer::registeredCustomers)
{
if (customer.isActive)
{
returnList.push_back(
funcField(c, customer));
}
}
return returnList;
}
vector<string> Customer::GetActiveCustomerByField(
const string &field)
{
function<string(const Customer&, Customer)> funct;
if (field == "name")
{
funct = &Customer::GetActiveCustomerNames;
}
else if (field == "address")
{
funct = &Customer::GetActiveCustomerAddresses;
}
else if (field == "phoneNumber")
{
funct = &Customer::GetActiveCustomerPhoneNumbers;
}
else if (field == "email")
{
funct = &Customer::GetActiveCustomerEmails;
}
else
{
throw invalid_argument("Unknown field");
}
return GetActiveCustomerByFunctionField(funct);
}
与Step01
代码相比,GetActiveCustomerNames()
、GetActiveCustomerAddresses()
、GetActiveCustomerPhoneNumbers()
和GetActiveCustomerEmails()
方法的实现现在更加简洁。它们只包含一行代码。然而,我们需要一个新的方法来容纳获取类的私有属性列表的过程,即GetActiveCustomerByField()
方法。该方法被传递给函数,使其成为一等函数,正如我们在前面的代码中所看到的。在这个Step02
文件夹中,main.cpp
代码应该如下所示:
/* Main.cpp - Step02 */
#include <iostream>
#include "Customer.h"
using namespace std;
void RegisterCustomers()
{
int i = 0;
bool b = false;
// Initialize name
vector<string> nameList =
{
"William",
"Aiden",
"Rowan",
"Jamie",
"Quinn",
"Haiden",
"Logan",
"Emerson",
"Sherlyn",
"Molly"
};
// Clear the registeredCustomers vector array
Customer::registeredCustomers.clear();
for (auto name : nameList)
{
// Create Customer object
// and fill all properties
Customer c;
c.id = i++;
c.name = name;
c.address = "somewhere";
c.phoneNumber = "0123";
c.email = name + "@xyz.com";
c.isActive = b;
// Flip the b value
b = !b;
// Send data to the registeredCustomers
Customer::registeredCustomers.push_back(c);
}
}
auto main() -> int
{
cout << "[Step02]" << endl;
cout << "--------" << endl;
// Fill the Customer::registeredCustomers
// with the content
RegisterCustomers();
// Instance Customer object
Customer customer;
// Get the active customer names
cout << "List of active customer names:" << endl;
vector<string> activeCustomerNames =
customer.GetActiveCustomerByField("name");
for (auto &name : activeCustomerNames)
{
cout << name << endl;
}
return 0;
}
正如我们在前面的main.cpp
代码中所看到的,我们现在将调用GetActiveCustomerByField()
方法,而不是像在Step01
中那样调用GetActiveCustomerNames()
。我们只需要将一个字符串数据类型的字段名传递给GetActiveCustomerNames()
方法,它就会调用适当的方法来检索属性值。例如,我们将检索name
属性值,因为我们在GetActiveCustomerByField()
方法中传递了name
。如果我们运行前面的Step02
代码,我们应该会看到以下的截图,这与我们在Step01
代码中看到的完全相同:
虽然我们的代码可以正常运行,但如果我们想要向类中添加更多的字段或属性,然后需要收集该新字段的列表,我们将面临一个问题。通过使用前面的代码,我们必须在GetActiveCustomerByFunctionField()
方法中添加一个新的else
部分。接下来,我们将找到解决方法来解决这个问题。
添加一个基类
如果我们想在类中添加更多字段,并且每次添加新字段时都希望轻松访问它的列表,我们必须创建一个从包含虚函数的基类派生出来的新类。通过这样做,我们可以派生出基类的虚方法,并为其实现正确的代码。在这里,我们还将获得模板元编程的能力,因为我们将设计基类为模板。基类的声明应该如下:
template<typename T, typename U>
class BaseClass
{
public:
virtual U InvokeFunction(
const std::shared_ptr<T>&) = 0;
};
现在,我们可以从基类派生出四个新类,用于类中的四种方法。类的声明应该如下:
class CustomerName :
public BaseClass<Customer, std::string>
{
public:
virtual std::string InvokeFunction(
const std::shared_ptr<Customer> &customer)
{
return customer->name;
}
};
class CustomerAddress :
public BaseClass<Customer, std::string>
{
public:
virtual std::string InvokeFunction(
const std::shared_ptr<Customer> &customer)
{
return customer->address;
}
};
class CustomerPhoneNumber :
public BaseClass<Customer, std::string>
{
public:
virtual std::string InvokeFunction(
const std::shared_ptr<Customer> &customer)
{
return customer->phoneNumber;
}
};
class CustomerEmail :
public BaseClass<Customer, std::string>
{
public:
virtual std::string InvokeFunction(
const std::shared_ptr<Customer> &customer)
{
return customer->email;
}
};
此外,GetActiveCustomerByFunctionField()
方法的参数类型也需要修改,因此方法的签名应该如下:
template<typename T>
static std::vector<T> GetActiveCustomerByFunctionField(
const std::shared_ptr<BaseClass<Customer, T>>
&classField);
此外,实现了前述代码的Step03
代码的完整头文件应该如下:
/* Customer.h - Step03 */
#ifndef __CUSTOMER_H__
#define __CUSTOMER_H__
#include <string>
#include <vector>
#include <memory>
class Customer
{
private:
template<typename T, typename U>
class BaseClass
{
public:
virtual U InvokeFunction(
const std::shared_ptr<T>&) = 0;
};
class CustomerName :
public BaseClass<Customer, std::string>
{
public:
virtual std::string InvokeFunction(
const std::shared_ptr<Customer> &customer)
{
return customer->name;
}
};
class CustomerAddress :
public BaseClass<Customer, std::string>
{
public:
virtual std::string InvokeFunction(
const std::shared_ptr<Customer> &customer)
{
return customer->address;
}
};
class CustomerPhoneNumber :
public BaseClass<Customer, std::string>
{
public:
virtual std::string InvokeFunction(
const std::shared_ptr<Customer> &customer)
{
return customer->phoneNumber;
}
};
class CustomerEmail :
public BaseClass<Customer, std::string>
{
public:
virtual std::string InvokeFunction(
const std::shared_ptr<Customer> &customer)
{
return customer->email;
}
};
public:
static std::vector<Customer> registeredCustomers;
int id = 0;
std::string name;
std::string address;
std::string phoneNumber;
std::string email;
bool isActive = true;
static std::vector<std::string> GetActiveCustomerNames();
static std::vector<std::string>
GetActiveCustomerAddresses();
static std::vector<std::string>
GetActiveCustomerPhoneNumbers();
static std::vector<std::string> GetActiveCustomerEmails();
template<typename T>
static std::vector<T> GetActiveCustomerByFunctionField(
const std::shared_ptr<BaseClass<Customer, T>>
&classField);
};
#endif // __CUSTOMER_H__
现在,每个前述类中的方法都有不同的任务,并且可以通过类名进行识别。我们还将修改GetActiveCustomerByFunctionField()
方法的实现,因为它现在传递了一个新的参数类型,即类名。通过传递一个类,现在更容易传递我们在类方法中的期望任务。GetActiveCustomerByFunctionField()
方法的实现应该如下:
template<typename T>
vector<T> Customer::GetActiveCustomerByFunctionField(
const shared_ptr<BaseClass<Customer, T>> &classField)
{
vector<T> returnList;
for (auto customer : Customer::registeredCustomers)
{
if (customer.isActive)
{
returnList.push_back(
classField->InvokeFunction(
make_shared<Customer>(customer)));
}
}
return returnList;
}
正如我们所看到的,前面的方法可以运行我们传递的类的方法,即classField
。此外,由于我们的类是从BaseClass
类派生出来的,我们可以通知方法接收类型为BaseClass
的参数。
现在我们可以实现在头文件中声明的公共方法--GetActiveCustomerNames()
、GetActiveCustomerAddresses()
、GetActiveCustomerPhoneNumbers()
和GetActiveCustomerEmails()
方法。这四个方法将调用GetActiveCustomerByFunctionField()
方法,并传递InvokeFunction()
方法的定义。代码应该如下:
vector<string> Customer::GetActiveCustomerNames()
{
return Customer::GetActiveCustomerByFunctionField<string>(
make_shared<CustomerName>());
}
vector<string> Customer::GetActiveCustomerAddresses()
{
return Customer::GetActiveCustomerByFunctionField<string>(
make_shared<CustomerAddress>());
}
vector<string> Customer::GetActiveCustomerPhoneNumbers()
{
return Customer::GetActiveCustomerByFunctionField<string>(
make_shared<CustomerPhoneNumber>());
}
vector<string> Customer::GetActiveCustomerEmails()
{
return Customer::GetActiveCustomerByFunctionField<string>(
make_shared<CustomerEmail>());
}
然后,我们将得到一个完整的Customer.cpp
文件,如下所示:
/* Customer.cpp - Step03 */
#include "Customer.h"
using namespace std;
vector<Customer> Customer::registeredCustomers;
vector<string> Customer::GetActiveCustomerNames()
{
return Customer::GetActiveCustomerByFunctionField<string>(
make_shared<CustomerName>());
}
vector<string> Customer::GetActiveCustomerAddresses()
{
return Customer::GetActiveCustomerByFunctionField<string>(
make_shared<CustomerAddress>());
}
vector<string> Customer::GetActiveCustomerPhoneNumbers()
{
return Customer::GetActiveCustomerByFunctionField<string>(
make_shared<CustomerPhoneNumber>());
}
vector<string> Customer::GetActiveCustomerEmails()
{
return Customer::GetActiveCustomerByFunctionField<string>(
make_shared<CustomerEmail>());
}
template<typename T>
vector<T> Customer::GetActiveCustomerByFunctionField(
const shared_ptr<BaseClass<Customer, T>> &classField)
{
vector<T> returnList;
for (auto &customer : Customer::registeredCustomers)
{
if (customer.isActive)
{
returnList.push_back(
classField->InvokeFunction(
make_shared<Customer>(customer)));
}
}
return returnList;
}
通过在Step03
文件夹中拥有Customer.h
和Customer.cpp
代码,我们现在更容易获取Customer
类中的属性列表。例如,如果我们想要检索活跃客户的列表,我们可以直接调用GetActiveCustomerNames()
方法,就像我们在下面的main.cpp
代码中看到的那样:
/* Main.cpp - Step03 */
#include <iostream>
#include "Customer.h"
using namespace std;
void RegisterCustomers()
{
int i = 0;
bool b = false;
// Initialize name
vector<string> nameList =
{
"William",
"Aiden",
"Rowan",
"Jamie",
"Quinn",
"Haiden",
"Logan",
"Emerson",
"Sherlyn",
"Molly"
};
// Clear the registeredCustomers vector array
Customer::registeredCustomers.clear();
for (auto name : nameList)
{
// Create Customer object
// and fill all properties
Customer c;
c.id = i++;
c.name = name;
c.address = "somewhere";
c.phoneNumber = "0123";
c.email = name + "@xyz.com";
c.isActive = b;
// Flip the b value
b = !b;
// Send data to the registeredCustomers
Customer::registeredCustomers.push_back(c);
}
}
auto main() -> int
{
cout << "[Step03]" << endl;
cout << "--------" << endl;
// Fill the Customer::registeredCustomers
// with the content
RegisterCustomers();
// Instance Customer object
Customer customer;
// Get the active customer names
cout << "List of active customer names:" << endl;
vector<string> activeCustomerNames =
customer.GetActiveCustomerNames();
for (auto &name : activeCustomerNames)
{
cout << name << endl;
}
return 0;
}
现在,让我们在Step03
文件夹中运行程序。我们应该在控制台上看到以下截图:
再次,我们得到了与上一步相同的输出。我们将在下一节中使Customer
类变得纯净。所以,继续前进!
将类转换为纯净的
正如我们在第二章中讨论的函数式编程中的函数操作,我们必须在函数式编程中创建一个纯函数以避免副作用。如果我们回到之前的GetActiveCustomerByFunctionField()
方法定义,它迭代了一个全局变量registeredCustomers
。这将是一个问题,因为GetActiveCustomerByFunctionField()
方法将提供一个不同的输出,尽管传递的参数完全相同。
为了解决这个问题,我们必须废除这个全局变量。然后,我们必须修改方法的定义如下:
template<typename T>
vector<T> Customer::GetActiveCustomerByFunctionField(
vector<Customer> customers,
const shared_ptr<BaseClass<Customer, T>>
&classField)
{
vector<T> returnList;
for (auto &customer : customers)
{
if (customer.isActive)
{
returnList.push_back(
classField->InvokeFunction(
make_shared<Customer>(customer)));
}
}
return returnList;
}
由于我们不再拥有registeredCustomers
属性,我们还必须将注册的客户列表传递给GetActiveCustomerByFunctionField()
方法。然后,该方法将迭代我们传递的客户列表,以找到活跃客户。此外,由于我们修改了方法签名,我们还必须修改Customer.h
文件中的方法声明,如下所示:
template<typename T>
static std::vector<T> GetActiveCustomerByFunctionField(
std::vector<Customer> customers,
const std::shared_ptr<BaseClass<Customer, T>>
&classField);
我们讨论了GetActiveCustomerByFunctionField()
方法是由Customer
类中的其他方法调用的。因此,我们还必须修改方法的实现,就像我们在下面的代码片段中看到的那样:
vector<string> Customer::GetActiveCustomerNames(
vector<Customer> customers)
{
return Customer::GetActiveCustomerByFunctionField<string>(
customers,
make_shared<CustomerName>());
}
vector<string> Customer::GetActiveCustomerAddresses(
vector<Customer> customer)
{
return Customer::GetActiveCustomerByFunctionField<string>(
customer,
make_shared<CustomerAddress>());
}
vector<string> Customer::GetActiveCustomerPhoneNumbers(
vector<Customer> customer)
{
return Customer::GetActiveCustomerByFunctionField<string>(
customer,
make_shared<CustomerPhoneNumber>());
}
vector<string> Customer::GetActiveCustomerEmails(
vector<Customer> customer)
{
return Customer::GetActiveCustomerByFunctionField<string>(
customer,
make_shared<CustomerEmail>());
}
我们还需要修改Customer.h
文件中的方法声明,如下面的代码片段所示:
static std::vector<std::string> GetActiveCustomerNames(
std::vector<Customer> customer);
static std::vector<std::string> GetActiveCustomerAddresses(
std::vector<Customer> customer);
static std::vector<std::string> GetActiveCustomerPhoneNumbers(
std::vector<Customer> customer);
static std::vector<std::string> GetActiveCustomerEmails(
std::vector<Customer> customer);
现在,Customer.h
文件将包含以下完整的代码块:
/* Customer.h - Step04 */
#ifndef __CUSTOMER_H__
#define __CUSTOMER_H__
#include <string>
#include <vector>
#include <memory>
class Customer
{
private:
template<typename T, typename U>
class BaseClass
{
public:
virtual U InvokeFunction(
const std::shared_ptr<T>&) = 0;
};
class CustomerName :
public BaseClass<Customer, std::string>
{
public:
virtual std::string InvokeFunction(
const std::shared_ptr<Customer> &customer)
{
return customer->name;
}
};
class CustomerAddress :
public BaseClass<Customer, std::string>
{
public:
virtual std::string InvokeFunction(
const std::shared_ptr<Customer> &customer)
{
return customer->address;
}
};
class CustomerPhoneNumber :
public BaseClass<Customer, std::string>
{
public:
virtual std::string InvokeFunction(
const std::shared_ptr<Customer> &customer)
{
return customer->phoneNumber;
}
};
class CustomerEmail :
public BaseClass<Customer, std::string>
{
public:
virtual std::string InvokeFunction(
const std::shared_ptr<Customer> &customer)
{
return customer->email;
}
};
public:
int id = 0;
std::string name;
std::string address;
std::string phoneNumber;
std::string email;
bool isActive = true;
static std::vector<std::string> GetActiveCustomerNames(
std::vector<Customer> customer);
static std::vector<std::string> GetActiveCustomerAddresses(
std::vector<Customer> customer);
static std::vector<std::string> GetActiveCustomerPhoneNumbers(
std::vector<Customer> customer);
static std::vector<std::string> GetActiveCustomerEmails(
std::vector<Customer> customer);
template<typename T>
static std::vector<T> GetActiveCustomerByFunctionField(
std::vector<Customer> customers,
const std::shared_ptr<BaseClass<Customer, T>>
&classField);
};
#endif // __CUSTOMER_H__
而且,Customer.cpp
文件将如下所示:
/* Customer.cpp - Step04 */
#include "Customer.h"
using namespace std;
vector<string> Customer::GetActiveCustomerNames(
vector<Customer> customers)
{
return Customer::GetActiveCustomerByFunctionField<string>(
customers,
make_shared<CustomerName>());
}
vector<string> Customer::GetActiveCustomerAddresses(
vector<Customer> customer)
{
return Customer::GetActiveCustomerByFunctionField<string>(
customer,
make_shared<CustomerAddress>());
}
vector<string> Customer::GetActiveCustomerPhoneNumbers(
vector<Customer> customer)
{
return Customer::GetActiveCustomerByFunctionField<string>(
customer,
make_shared<CustomerPhoneNumber>());
}
vector<string> Customer::GetActiveCustomerEmails(
vector<Customer> customer)
{
return Customer::GetActiveCustomerByFunctionField<string>(
customer,
make_shared<CustomerEmail>());
}
template<typename T>
vector<T> Customer::GetActiveCustomerByFunctionField(
vector<Customer> customers,
const shared_ptr<BaseClass<Customer, T>>
&classField)
{
vector<T> returnList;
for (auto &customer : customers)
{
if (customer.isActive)
{
returnList.push_back(
classField->InvokeFunction(
make_shared<Customer>(customer)));
}
}
return returnList;
}
由于Customer
类已经更改,不再有registeredCustomer
变量,我们还需要修改main.cpp
文件中的RegisterCustomers()
方法。方法的先前版本没有返回值。现在,我们将使代码返回客户列表。我们还需要修改main()
方法,因为我们必须在Main.cpp
文件中使用新的RegisterCustomers()
方法。该文件将包含以下代码块:
/* Main.cpp - Step04 */
#include <iostream>
#include "Customer.h"
using namespace std;
vector<Customer> RegisterCustomers()
{
int i = 0;
bool b = false;
vector<Customer> returnValue;
// Initialize name
vector<string> nameList =
{
"William",
"Aiden",
"Rowan",
"Jamie",
"Quinn",
"Haiden",
"Logan",
"Emerson",
"Sherlyn",
"Molly"
};
for (auto name : nameList)
{
// Create Customer object
// and fill all properties
Customer c;
c.id = i++;
c.name = name;
c.address = "somewhere";
c.phoneNumber = "0123";
c.email = name + "@xyz.com";
c.isActive = b;
// Flip the b value
b = !b;
// Send data to the registeredCustomers
returnValue.push_back(c);
}
return returnValue;
}
auto main() -> int
{
cout << "[Step04]" << endl;
cout << "--------" << endl;
// Instance Customer object
Customer customer;
// Get the active customer names
cout << "List of active customer names:" << endl;
vector<string> activeCustomerNames =
customer.GetActiveCustomerNames(
RegisterCustomers());
for (auto name : activeCustomerNames)
{
cout << name << endl;
}
return 0;
}
正如我们在前面的main()
方法中所看到的,我们调用了GetActiveCustomerNames()
方法,并传递了RegisterCustomers()
方法的结果。现在,让我们尝试在Step06
文件夹中运行程序。当我们运行程序时,控制台应该输出以下内容:
再次,我们得到了与之前步骤中相同的输出,但采用了函数式编程的新方法。接下来,我们将重构代码,使用 Lambda 表达式来简化过滤任务。
过滤条件并实现 Lambda 表达式
让我们专注于GetActiveCustomerByFunctionField()
方法。在那里,我们可以找到一个if
结构来过滤活跃客户。正如我们在前几章中讨论的,我们可以使用copy_if()
方法来过滤条件。以下代码片段实现了copy_if()
方法来过滤活跃客户:
template<typename T>
vector<T> Customer::GetActiveCustomerByFunctionField(
vector<Customer> customers,
const shared_ptr<BaseClass<Customer, T>>
&classField)
{
vector<Customer> activeCustomers;
vector<T> returnList;
copy_if(
customers.begin(),
customers.end(),
back_inserter(activeCustomers),
[](Customer customer)
{
if (customer.isActive)
return true;
else
return false;
});
for (auto &customer : customers)
{
if (customer.isActive)
{
returnList.push_back(
classField->InvokeFunction(
make_shared<Customer>(customer)));
}
}
return returnList;
}
正如我们在前面的代码片段中所看到的,我们创建了一个匿名方法,如果我们传递的客户实例是活跃的,则返回 true。此外,我们还可以重构前面的GetActiveCustomerByFunctionField()
方法,使其再次使用匿名方法,如下面的代码片段所示:
template<typename T>
vector<T> Customer::GetActiveCustomerByFunctionField(
vector<Customer> customers,
const shared_ptr<BaseClass<Customer, T>>
&classField)
{
vector<Customer> activeCustomers;
vector<T> returnList;
copy_if(
customers.begin(),
customers.end(),
back_inserter(activeCustomers),
[](Customer customer)
{
if (customer.isActive)
return true;
else
return false;
});
for_each(
activeCustomers.begin(),
activeCustomers.end(),
&returnList, &classField
{
returnList.push_back(
classField->InvokeFunction(
make_shared<Customer>(customer))
);
});
return returnList;
}
除了使用 Lambda 表达式实现过滤技术之外,我们还将在Customer
类中添加一个名为CountActiveCustomers()
的方法。该方法将计算活跃客户的数量。该方法的定义应该如下:
int Customer::CountActiveCustomers(
vector<Customer> customer)
{
int add = 0;
for (auto cust : customer)
{
// Adding 1 if the customer is active
if(cust.isActive)
++add;
}
return add;
}
现在,我们将在Step05
代码块中有Customer.cpp
代码如下:
/* Customer.cpp - Step05 */
#include <algorithm>
#include "Customer.h"
using namespace std;
vector<string> Customer::GetActiveCustomerNames(
vector<Customer> customers)
{
return Customer::GetActiveCustomerByFunctionField<string>(
customers,
make_shared<CustomerName>());
}
vector<string> Customer::GetActiveCustomerAddresses(
vector<Customer> customer)
{
return Customer::GetActiveCustomerByFunctionField<string>(
customer,
make_shared<CustomerAddress>());
}
vector<string> Customer::GetActiveCustomerPhoneNumbers(
vector<Customer> customer)
{
return Customer::GetActiveCustomerByFunctionField<string>(
customer,
make_shared<CustomerPhoneNumber>());
}
vector<string> Customer::GetActiveCustomerEmails(
vector<Customer> customer)
{
return Customer::GetActiveCustomerByFunctionField<string>(
customer,
make_shared<CustomerEmail>());
}
int Customer::CountActiveCustomers(
vector<Customer> customer)
{
int add = 0;
for (auto cust : customer)
{
// Adding 1 if the customer is active
if(cust.isActive)
++add;
}
return add;
}
template<typename T>
vector<T> Customer::GetActiveCustomerByFunctionField(
vector<Customer> customers,
const shared_ptr<BaseClass<Customer, T>>
&classField)
{
vector<Customer> activeCustomers;
vector<T> returnList;
copy_if(
customers.begin(),
customers.end(),
back_inserter(activeCustomers),
[](Customer customer)
{
if (customer.isActive)
return true;
else
return false;
});
for_each(
activeCustomers.begin(),
activeCustomers.end(),
&returnList, &classField
{
returnList.push_back(
classField->InvokeFunction(
make_shared<Customer>(customer))
);
});
return returnList;
}
不要忘记修改Customer.h
文件,因为我们已经向类中添加了一个新方法。该文件应包含以下代码片段:
/* Customer.h - Step05 */
#ifndef __CUSTOMER_H__
#define __CUSTOMER_H__
#include <string>
#include <vector>
#include <memory>
class Customer
{
private:
template<typename T, typename U>
class BaseClass
{
public:
virtual U InvokeFunction(
const std::shared_ptr<T>&) = 0;
};
class CustomerName :
public BaseClass<Customer, std::string>
{
public:
virtual std::string InvokeFunction(
const std::shared_ptr<Customer> &customer)
{
return customer->name;
}
};
class CustomerAddress :
public BaseClass<Customer, std::string>
{
public:
virtual std::string InvokeFunction(
const std::shared_ptr<Customer> &customer)
{
return customer->address;
}
};
class CustomerPhoneNumber :
public BaseClass<Customer, std::string>
{
public:
virtual std::string InvokeFunction(
const std::shared_ptr<Customer> &customer)
{
return customer->phoneNumber;
}
};
class CustomerEmail :
public BaseClass<Customer, std::string>
{
public:
virtual std::string InvokeFunction(
const std::shared_ptr<Customer> &customer)
{
return customer->email;
}
};
public:
int id = 0;
std::string name;
std::string address;
std::string phoneNumber;
std::string email;
bool isActive = true;
static std::vector<std::string> GetActiveCustomerNames(
std::vector<Customer> customer);
static std::vector<std::string> GetActiveCustomerAddresses(
std::vector<Customer> customer);
static std::vector<std::string> GetActiveCustomerPhoneNumbers(
std::vector<Customer> customer);
static std::vector<std::string> GetActiveCustomerEmails(
std::vector<Customer> customer);
static int CountActiveCustomers(
std::vector<Customer> customer);
template<typename T>
static std::vector<T> GetActiveCustomerByFunctionField(
std::vector<Customer> customers,
const std::shared_ptr<BaseClass<Customer, T>>
&classField);
};
#endif // __CUSTOMER_H__
现在,我们将在我们的main()
函数中调用CountActiveCustomers()
方法。我们将通过检查以下Main.cpp
代码块来看看我们是如何做到的:
/* Main.cpp - Step05 */
#include <iostream>
#include <chrono>
#include "Customer.h"
using namespace std;
vector<Customer> RegisterCustomers()
{
int i = 0;
bool b = false;
vector<Customer> returnValue;
// Initialize name
vector<string> nameList =
{
"William",
"Aiden",
"Rowan",
"Jamie",
"Quinn",
"Haiden",
"Logan",
"Emerson",
"Sherlyn",
"Molly"
};
for (auto name : nameList)
{
// Create Customer object
// and fill all properties
Customer c;
c.id = i++;
c.name = name;
c.address = "somewhere";
c.phoneNumber = "0123";
c.email = name + "@xyz.com";
c.isActive = b;
// Flip the b value
b = !b;
// Send data to the registeredCustomers
returnValue.push_back(c);
}
return returnValue;
}
auto main() -> int
{
cout << "[Step05]" << endl;
cout << "--------" << endl;
// Recording start time for the program
auto start = chrono::high_resolution_clock::now();
// Instance Customer object
Customer customer;
// Counting active customers
cout << "Total active customers: " << endl;
cout << customer.CountActiveCustomers(
RegisterCustomers());
cout << endl << "--------" << endl;
// Get the active customer names
cout << "List of active customer names:" << endl;
vector<string> activeCustomerNames =
customer.GetActiveCustomerNames(
RegisterCustomers());
for (auto name : activeCustomerNames)
{
cout << name << endl;
}
// Recording end time for the program
auto finish = chrono::high_resolution_clock::now();
// Calculating the elapsed time for the program
chrono::duration<double, milli> elapsed = finish - start;
// Displaying elapsed time for the program
cout << "--------" << endl;
cout << "Total consuming time = ";
cout << elapsed.count() << " milliseconds" << endl;
return 0;
}
正如我们在前面的代码中所看到的,我们调用了CountActiveCustomers()
方法,并将RegisterCustomers()
方法的输出作为参数传递。我们还添加了一个简单的秒表来计算程序运行所需的时间。前面代码的输出应该如下:
正如我们所看到的,我们需要0.997
毫秒来运行这一步的代码。然而,我们可以通过实现递归和记忆化来优化前面的代码,使其运行更快,这将在下一节中讨论。
事实上,我们可以通过运行activeCustomerNames.size()
方法来找出活跃客户的总数,以获取运行以下代码行后向量中元素的数量:
vector<string> activeCustomerNames =
customer.GetActiveCustomerNames(RegisterCustomers())`
然而,前面的代码示例想要向我们展示如何将for
循环转换为递归,以优化执行速度。我们将在接下来讨论这个问题
部分。
在Customer
类中实现递归和记忆化技术
如果我们看一下Step05
中CountActiveCustomers()
方法的定义,我们使用for
循环来计算活跃客户。然而,我们可以重写该方法以使用递归技术。让我们看一下以下代码,这是CountActiveCustomers()
方法的新定义:
int Customer::CountActiveCustomers(
vector<Customer> customer)
{
if(customer.empty())
return 0;
else
{
// Adding 1 if the customer is active
int add = customer.front().isActive ? 1 : 0;
// Removing the first element of vector
// It's similar with removing head
// and pass the tail
customer.erase(customer.begin());
// Running the recursion
return add + CountActiveCustomers(
customer);
}
}
正如我们在上面的代码片段中所看到的,我们使用尾递归来实现CountActiveCustomers()
方法。我们只需要在customer
向量中找到一个活跃的客户时每次增加add
变量。然后,代码会删除customer
向量的第一个元素,并将其传递给CountActiveCustomers()
方法。我们递归这个过程,直到customer
向量的元素为空为止。
此外,我们还使用了我们在第五章中讨论的Memoization
类,即使用惰性求值推迟执行过程,来优化我们的代码。我们将修改Main.cpp
文件中的main()
函数,使main()
函数包含以下代码片段:
auto main() -> int
{
cout << "[Step06]" << endl;
cout << "--------" << endl;
// Recording start time for the program
auto start = chrono::high_resolution_clock::now();
// Instance Customer object
Customer customer;
// Counting active customers
cout << "Total active customers: " << endl;
cout << customer.CountActiveCustomers(
RegisterCustomers());
cout << endl << "--------" << endl;
// Initializing memoization instance
Memoization<vector<string>> custMemo(
[customer]()
{
return customer.GetActiveCustomerNames(
RegisterCustomers());
});
// Get the active customer names
cout << "List of active customer names:" << endl;
vector<string> activeCustomerNames =
custMemo.Fetch();
for (auto name : activeCustomerNames)
{
cout << name << endl;
}
// Recording end time for the program
auto finish = chrono::high_resolution_clock::now();
// Calculating the elapsed time for the program
chrono::duration<double, milli> elapsed = finish - start;
// Displaying elapsed time for the program
cout << "--------" << endl;
cout << "Total consuming time = ";
cout << elapsed.count() << " milliseconds" << endl;
return 0;
}
在上面的代码片段中,我们现在通过调用Fetch()
方法从Memoization
实例运行GetActiveCustomerNames()
方法。如果我们运行Step06
代码,我们应该在控制台上看到以下输出:
现在,该代码只需要0.502
毫秒来运行。与Step05
代码相比,代码执行的速度几乎快了一倍。这证明,通过使用功能性方法,我们不仅可以获得更好的代码结构,还可以进行速度优化。
调试代码
有时,在编码过程中,当我们运行代码时,一个或多个变量会出现意外的结果。这可能发生在执行过程中。为了避免陷入这种情况,我们可以通过逐步运行程序来分析我们的程序。我们可以使用 GCC 编译器中包含的调试器工具--GDB(GNU 项目调试器)。这个工具允许我们弄清楚目标程序在执行时发生了什么,或者在它崩溃时正在做什么。在本节中,我们将应用 GDB 来简化我们的编程任务,并找到问题的解决方案并处理它。
启动调试工具
现在,让我们准备要分析的可执行文件。我们将使用Step01
文件夹中的代码,因为它是一个简单的代码,我们可以很容易地从中学习。我们必须使用-g
选项重新编译代码,并将可执行文件命名为customer.exe
。以下是编译代码以便进行调试的三个命令:
g++ -Wall -g -c Main.cpp -o Main.o
g++ -Wall -g -c Customer.cpp -o Customer.o
g++ Main.o Customer.o -o Customer.exe
GDB 只能分析包含调试信息和符号的可执行文件,这些信息和符号在调试过程中很重要。我们可以在编译源代码时插入-g
选项,以便将调试信息和符号添加到可执行文件中。
在控制台上输入gdb customer
将打开调试器工具,并加载来自customer.exe
文件的调试器信息和符号。然后我们将在控制台上看到以下截图:
正如我们在上一张截图中所看到的,它已成功从customer.exe
文件中读取了符号。然后,在 GDB 控制台中输入start
来启动分析过程。调试器将在main()
方法的第一行创建一个临时断点。启动 GDB 后,我们将在控制台上看到以下截图:
现在,程序正在进行调试过程。我们可以继续分析程序的运行情况。在下一节中,我们可以选择逐步进行或者运行程序直到找到下一个断点。
要开始调试过程,我们可以调用run
或start
命令。前者将在 GDB 下启动我们的程序,而后者将类似地行为,但将逐行执行代码。区别在于,如果我们还没有设置断点,程序将像调用run
命令时一样运行,而调试器将自动在主代码块中设置断点,如果我们使用start
命令开始,程序将在达到该断点时停止。
继续和步进调试过程
在前面的部分中有三个继续步骤的命令。它们如下:
-
continue
: 这将恢复程序的执行,直到程序正常完成。如果找到断点,执行将停在设置断点的行。 -
step
: 这只执行程序的下一步。步骤可能是源代码的一行,也可能是一条机器指令。如果找到函数的调用,它将进入函数并在函数内再运行一步。 -
next
: 这将继续执行当前堆栈帧中的下一行。换句话说,如果next
命令找到函数的调用,它将不会进入函数。
由于我们还没有设置断点,让我们输入next
命令,这样调试指针就会移到代码的下一行。我们将多次运行next
命令,直到到达代码的末尾(或者直到我们看到进程正常退出)。当我们多次应用next
命令时,应该看到以下截图:
正如我们在前面的截图中看到的,我们可以逐步运行程序来分析我们的程序。接下来,如果我们有怀疑的对象需要分析,我们将设置断点。
我们只需要按下Enter
键来运行 GDB 中的上一个命令。按下Q键将使调试控制台退出到窗口控制台。
设置和删除断点
让我们通过输入Q键退出调试控制台。我们需要重新启动调试,因此我们需要在窗口控制台上再次输入gdb customer
。之后,我们不需要输入start
命令,而是在继续进程之前设置断点。在 GDB 控制台中分别输入break 68
和break Customer.cpp:15
。输出如下所示:
现在,我们在不同文件中有两个断点--Main.cpp
和Customer.cpp
。我们现在可以在 GDB 控制台中输入run
来启动调试器,如下截图所示:
由于调试器首先命中了GetActiveCustomerNames()
方法,它停在我们在该方法中设置断点的行,即Customer.cpp
文件中的第15
行。只需输入continue
命令,然后多次按Enter,直到它在Main.cpp
文件的第69
行命中断点。
打印对象值
让我们通过在Main.cpp
文件的第68
行设置断点,然后启动调试器,直到它命中断点。命中断点后,输入print name
来查看name
变量的值。以下截图显示了该过程的步骤:
正如我们在前面的截图中看到的,name
变量的值是Aiden
。我们可以通过输入continue
命令来继续调试,这样调试器就会再次在for
循环中触发断点,然后输入print name
来找出下一个名称的值。
GDB 中有很多命令,如果在本书中写出来会很多。如果您需要在 GDB 中找到更多命令,请参考以下链接:
www.gnu.org/software/gdb/documentation/
总结
在本书的最后一章中,我们成功地通过重构将功能类从命令式类中开发出来,我们可以使用它来创建一个更复杂的程序。我们实现了前几章学到的知识。我们还讨论了调试技术,这是一种在面对意外结果或程序中间崩溃时非常有用的武器。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
2022-05-04 Impatient JavaScript 中文版校对活动期待大家的参与
2022-05-04 ApacheCN 翻译/校对活动进度公告 2022.5.4
2022-05-04 非安全系列教程 NPM、PYPI、DockerHub 备份
2022-05-04 UIUC CS241 系统编程中文讲义校对活动 | ApacheCN