我们在这本书里已经讨论了很多主题,所以现在是时候把它们集中在一个方便的章节里,你可以用它来帮助记住如何使用我们介绍的函数式编程技术。我们也将借此机会看看 C++ 20 标准,并提及我们如何在代码中使用这些新功能。
本章将涵盖以下主题:
- 支持用 C++ 编写纯函数的方法,以及未来的建议
- 支持用 C++ 编写 lambdas 的方式,以及未来的建议
- 支持的 C++ 方式,以及未来的建议
- C++ 中支持的函数组合方式以及未来的建议
您将需要一个支持 C++ 17 的编译器;我用的是 GCC 7.4.0c.
代码在的 GitHub 上。com/ PacktPublishing/动手-函数-用- Cpp 编程Chapter16
文件夹中的。它包含并使用了doctest
,这是一个单头开源单元测试库。你可以在的 GitHub 资源库中找到它。com/ onqtam/ doctest 。
到目前为止,我们已经探索了用 C++ 编写函数式代码的几种方法。现在,我们将看看 C++ 17 标准允许的一些附加选项,以及 C++ 20 允许的一些选项。所以,让我们从编写纯函数开始。
纯函数是接收相同输入时返回相同输出的函数。它们的可预测性使它们有助于理解编写的代码如何与其运行时性能相关联。
我们在第二章、*理解纯函数、*中发现,在 C++ 中编写纯函数需要结合const
和static
,这取决于函数是类的一部分还是自由函数,以及我们如何将参数传递给函数。为了方便起见,我将在这里重现我们对纯函数语法得出的结论:
- 类函数,按值传递:
static int increment(const int value)
int increment(const int value) const
- 类函数,通过引用传递:
static int increment(const int& value)
int increment(const int&value) const
- 类函数,按值传递指针:
static const int* increment(const int* value)
const int* increment(const int* value) const
- 类函数,通过引用传递指针:
static const int* increment(const int* const& value)
const int* increment(const int* const& value) const
- 独立功能,通过值
int increment(const int value)
- 独立功能,通过引用传递
int increment(const int& value)
- 独立功能,通过数值传递指针
const int* increment(const int* value)
- 独立功能,通过引用传递指针
const int* increment(const int* const& value)
我们还发现,虽然编译器有助于减少副作用,但它并不总是告诉我们一个函数是否是纯函数。在编写纯函数时,我们总是需要记住使用这三个标准,并小心应用它们:
- 对于相同的输入值,它总是返回相同的输出值。
- 它没有副作用。
- 它不会更改其参数值。
Lambdas 是函数式编程的一个基本部分,允许我们用函数进行操作。C++ 从 C++ 11 开始就有了 lambdas,但是最近对语法进行了一些补充。此外,我们将探索一些 lambda 特性,这些特性在本书中到目前为止还没有使用过,但是对于您自己的代码来说可以派上用场。
让我们从一个简单的λ开始— increment
有一个输入,并返回递增的值:
TEST_CASE("Increment"){
auto increment = [](auto value) { return value + 1;};
CHECK_EQ(2, increment(1));
}
方括号([]
)指定了捕获值的列表,我们将在下面的代码中看到。我们可以像对任何函数一样指定参数的类型:
TEST_CASE("Increment"){
auto increment = [](int value) { return value + 1;};
CHECK_EQ(2, increment(1));
}
我们也可以在参数列表和一个->
符号之后立即指定返回值:
TEST_CASE("Increment"){
auto increment = [](int value) -> int { return value + 1;};
CHECK_EQ(2, increment(1));
}
如果没有输入值,参数列表和圆括号()
可以忽略:
TEST_CASE("One"){
auto one = []{ return 1;};
CHECK_EQ(1, one());
}
我们可以通过指定值的名称来捕获它,在这种情况下,它是通过复制来捕获的:
TEST_CASE("Capture value"){
int value = 5;
auto addToValue = [value](int toAdd) { return value + toAdd;};
CHECK_EQ(6, addToValue(1));
}
或者,我们可以通过引用捕获一个值,使用捕获规范中的&
运算符:
TEST_CASE("Capture value by reference"){
int value = 5;
auto addToValue = [&value](int toAdd) { return value + toAdd;};
CHECK_EQ(6, addToValue(1));
}
如果我们捕获多个值,我们可以枚举它们,也可以只捕获所有的值。对于按值捕获,我们使用=
说明符:
TEST_CASE("Capture all values by value"){
int first = 5;
int second = 10;
auto addToValues = [=](int toAdd) { return first + second +
toAdd;};
CHECK_EQ(16, addToValues(1));
}
为了通过引用捕获所有值,我们使用没有任何变量名的&
说明符:
TEST_CASE("Capture all values by reference"){
int first = 5;
int second = 10;
auto addToValues = [&](int toAdd) { return first + second +
toAdd;};
CHECK_EQ(16, addToValues(1));
}
虽然不推荐,但我们可以在参数列表后使用mutable
说明符使 lambda 调用可变:
TEST_CASE("Increment mutable - NOT RECOMMENDED"){
auto increment = [](int& value) mutable { return ++ value;};
int value = 1;
CHECK_EQ(2, increment(value));
CHECK_EQ(2, value);
}
另外,从 C++ 20 开始,我们可以指定函数调用为consteval
,而不是默认的constexpr
:
TEST_CASE("Increment"){
auto one = []() consteval { return 1;};
CHECK_EQ(1, one());
}
不幸的是,g++ 8 还不支持这个用例。
异常说明符也是可能的;也就是说,如果λ抛出没有异常,那么noexcept
可能会派上用场:
TEST_CASE("Increment"){
auto increment = [](int value) noexcept { return value + 1;};
CHECK_EQ(2, increment(1));
}
如果 lambda 引发异常,可以将其指定为一般异常或特定异常:
TEST_CASE("Increment"){
auto increment = [](int value) throw() { return value + 1;};
CHECK_EQ(2, increment(1));
}
但是如果您想使用泛型类型呢?嗯,在 C++ 11 中,这个可以用function<>
类型。从 C++ 20 开始,所有优秀的类型约束都可以用简洁的语法为您的 lambdas 提供:
TEST_CASE("Increment"){
auto increment = [] <typename T>(T value) -> requires
NumericType<T> { return value + 1;};
CHECK_EQ(2, increment(1));
}
不幸的是,这在 g++ 8 中也不被支持。
部分应用是指通过在1
(或更多,但少于 N )参数上应用带有 N 参数的函数来获得新函数。
我们可以通过实现传递参数的函数或 lambda 来手动实现部分应用。这里有一个部分应用的例子,它使用std::plus
函数通过将其参数之一设置为1
来获得increment
函数:
TEST_CASE("Increment"){
auto increment = [](const int value) { return plus<int>()(value,
1); };
CHECK_EQ(2, increment(1));
}
在本书中,我们主要关注如何在这些情况下使用 lambdas 然而,值得一提的是,我们可以为同一个目标使用纯函数。例如,同一个增量函数可以写成一个普通的 C++ 函数:
namespace Increment{
int increment(const int value){
return plus<int>()(value, 1);
};
}
TEST_CASE("Increment"){
CHECK_EQ(2, Increment::increment(1));
}
部分应用可以借助bind()
函数在 C++ 中完成。bind()
函数允许我们将参数绑定到函数的值,允许我们从plus
导出increment
函数,如下所示:
TEST_CASE("Increment"){
auto increment = bind(plus<int>(), _1, 1);
CHECK_EQ(2, increment(1));
}
bind
取以下参数:
- 我们要绑定的函数。
- 要绑定到的参数;这些可以是一个值,也可以是一个占位符(如
_1
、_2
等)。占位符允许将参数转发给最终函数。
在纯函数式编程语言中,部分应用与 currying 相联系。 Currying 是将一个接受 N 个参数的函数分解成接受一个参数的 N 个函数。在 C++ 中没有标准的方法来咖喱一个函数,但是我们可以通过使用 lambdas 来做。让我们看一个实现pow
功能的例子:
auto curriedPower = [](const int base) {
return [base](const int exponent) {
return pow(base, exponent);
};
};
TEST_CASE("Power and curried power"){
CHECK_EQ(16, pow(2, 4));
CHECK_EQ(16, curriedPower(2)(4));
}
如您所见,在 currying 的帮助下,我们可以通过简单地调用 curried 函数来完成部分应用,该函数只有一个参数,而不是两个:
auto powerOf2 = curriedPower(2);
CHECK_EQ(16, powerOf2(4));
默认情况下,这种机制在许多纯函数式编程语言中都是启用的。然而,在 C++ 中更难做到。currying 没有标准支持,但是我们可以创建自己的curry
函数,该函数采用现有函数并返回其 curried 形式。这里有一个带有两个参数的函数的广义curry
函数的例子:
template<typename F>
auto curry2(F f){
return [=](auto first){
return [=](auto second){
return f(first, second);
};
};
}
此外,下面是我们如何使用它来咖喱和做部分应用:
TEST_CASE("Power and curried power"){
auto power = [](const int base, const int exponent){
return pow(base, exponent);
};
auto curriedPower = curry2(power);
auto powerOf2 = curriedPower(2);
CHECK_EQ(16, powerOf2(4));
}
现在让我们看看实现功能组合的方法。
功能组合是指取两个功能, f 和 g ,获得一个新的功能,*h;*对于任何值, h(x) = f(g(x)) 。我们可以在 lambda 或普通函数中手动实现函数组合。例如,给定两个函数,powerOf2
,计算2
的幂,和increment
,增加一个值,我们将看到以下内容:
auto powerOf2 = [](const int exponent){
return pow(2, exponent);
};
auto increment = [](const int value){
return value + 1;
};
我们可以通过简单地将调用封装到一个名为incrementPowerOf2
的 lambda 中来编写它们:
TEST_CASE("Composition"){
auto incrementPowerOf2 = [](const int exponent){
return increment(powerOf2(exponent));
};
CHECK_EQ(9, incrementPowerOf2(3));
}
或者,我们可以使用一个简单的函数,如下所示:
namespace Functions{
int incrementPowerOf2(const int exponent){
return increment(powerOf2(exponent));
};
}
TEST_CASE("Composition"){
CHECK_EQ(9, Functions::incrementPowerOf2(3));
}
然而,一个接受两个函数并返回组合函数的运算符很方便,它在许多编程语言中都有实现。C++ 中最接近函数组合运算符的是范围库中的|
管道运算符,该运算符目前在 C++ 20 标准中。然而,虽然它实现了组合,但它不适用于一般函数或 lambdas。幸运的是,C++ 是一种强大的语言,我们可以编写自己的合成函数,正如我们在第 4 章、函数合成的思想中所发现的:
template <class F, class G>
auto compose(F f, G g){
return [=](auto value){return f(g(value));};
}
TEST_CASE("Composition"){
auto incrementPowerOf2 = compose(increment, powerOf2);
CHECK_EQ(9, incrementPowerOf2(3));
}
回到范围库和管道操作器,我们可以在范围的上下文中使用这种形式的函数组合。我们已经在第 14 章、中使用范围库对这个主题进行了广泛的探讨,这里有一个使用管道运算符计算集合中所有既是2
又是3
的倍数的数字总和的例子:
auto isEven = [](const auto number){
return number % 2 == 0;
};
auto isMultipleOf3 = [](const auto number){
return number % 3 == 0;
};
auto sumOfMultiplesOf6 = [](const auto& numbers){
return ranges::accumulate(
numbers | ranges::view::filter(isEven) |
ranges::view::filter(isMultipleOf3), 0);
};
TEST_CASE("Sum of even numbers and of multiples of 6"){
list<int> numbers{1, 2, 5, 6, 10, 12, 17, 25};
CHECK_EQ(18, sumOfMultiplesOf6(numbers));
}
如您所见,标准 C++ 中有多种函数编程选项,C++ 20 中也有一些令人兴奋的发展。
就是这里!我们已经快速浏览了函数式编程中最重要的操作,以及如何使用 C++ 17 和 C++ 20 实现它们。我相信您现在在工具包中拥有了更多的工具——包括纯函数、lambdas、部分应用、currying 和函数组合,仅举几个例子。
从现在开始,如何使用它们是你的选择。挑选几个,或者将它们组合起来,或者根据可变状态慢慢地将代码移动到不变状态;掌握这些工具将使您在编写代码时有更多的选择和灵活性。
无论你选择做什么,我都祝你项目和编程生涯好运。快乐编码!