关于std::accumulate算法的知识
一、简介
如果有一个算法可以做各种各样的事情,那一定是std::accumulate
。重要的是要知道怎么使用它,以及什么时候不使用它。
关于std::accumulate
首先要知道的是它的头文件是<numeric>
,不像其他算法的头文件在<algorithm>
。
std::accumulate
用于汇总一个范围。即std::accumulate
接受一个元素集合,并且只返回一个值。它可以用于实现各种算法,但同时也要谨慎使用,否则会造成代码难以理解。
二、基本用法
2.1、数值类型
如果不指定任何内容,则std::accumulate
对其所取范围内的所有元素求和。这个和是用operator+
完成的。由于需要两个值来调用operator+
,因此还需要一个初始值来启动算法。
函数原型:
template<typename InputIterator, typename T>
T accumulate(InputIterator first, InputIterator last, T initialValue);
因此,对于一个数字集合,std::accumulate
将它们相加:
std::vector<int> numbers = { 2, 9, -4, 2 };
int sum = std::accumulate(begin(numbers), end(numbers), 0);
这里有个小陷阱。上面的代码适用于整型,但看看这段非整型的代码:
std::vector<double> doubles = { 1.5, 2, 3.5 };
double sum = std::accumulate(begin(doubles), end(doubles), 0);
结果将会输出6
。我们期望的是1.5 + 2 + 3.5 = 7
,而不是6
。要理解发生了什么,再看一下std::accumulate
的原型:
template<typename InputIterator, typename T>
T accumulate(InputIterator first, InputIterator last, T initialValue);
注意,类型T
不一定与范围内元素的类型相关。在调用时,它是从第三个参数0
推导出来的。而0
是一个整型!所以T
是int
。因此,std::accumulate
与int
一起工作,并截断每个求和的结果。
一个简单的修复方法是第三个参数传递一个double
类型:
std::vector<double> doubles = { 1.5, 2, 3.5 };
double sum = std::accumulate(begin(doubles), end(doubles), 0.);
这时的结果才是7
。这个例子值得关注,因为在这个例子中,代码编译和失败都是通过的。
2.2、其他类型
除了数字类型,没有什么可以阻止std::accumulate
在其他类型上使用。任何实现operator+
的类型都可以选择std::accumulate
。
在std::string
上,使用operator+
执行串联操作:
std::vector<std::string> words = { "Winter ", "is ", "Coming." };
std::string sentence = std::accumulate(begin(words), end(words), std::string(""));
这里需要注意,需要传递std::string("")
而不仅仅是""
作为初始值,因为后者导致T
是const char*
而不是std::string
,并且无法编译。
另外,即使范围内元素的类型没有实现operator+
,它仍然可以使用std::accumulate
和它的第二个重载,该重载接受一个函数(或函数对象)来代替operator+
。这个函数的两个参数甚至可以是不同的类型。
举例:有一个可以载几个人的电梯,目前他们的总重量小于一定的限制。下面的代码计算电梯中人群的总重量:
double totalWeight = std::accumulate(begin(group), end(group), 0.,
[](double currentWeight, Person const& person) {
return currentWeight + person.getWeight();
});
看看算法的最后一个参数。它代表一个函数(这里是一个 lambda 表达式),该函数接受一个初始值(这里为 0.
)和一个要“吸收”到当前值中的新元素。算法在“吸收”或“累积”了整个范围中的所有元素之后返回当前值。
三、合理利用 std::accumulate
这种过度设计提供了很多可能性。但是其中一些应该避免,因为它们会导致难以理解的代码,甚至在某些情况下需要用很大精力来解开。
原则:std::accumulate
将一个范围的总结模型化为一个值。但要注意的是,它不模拟函数应用。
引用前面的例子,想要计算电梯中每个人的体重可以使用std::accumulate
以以下方式实现:
std::accumulate(begin(group), end(group), &weights,
[](std::vector<double>* currentWeights, Person const& person) {
currentWeights->push_back(person.getWeight());
return currentWeights;
});
但这是错误的。记住,这是很容易犯的错误,不要这样实现代码。为什么是错的?因为这段代码遍历一个范围,对每个元素应用一个函数,并将结果放入一个新的集合中。这是std::transform
在代码中要表达的内容。
相反,这段代码使用std::accumulate
来将一个范围汇总为一个值,并扭曲了它的用法。其结果是,大量的代码并不能说明什么,而且还告诉它是错误的。换句话说,它扼杀了代码的表现力。
为了让代码更有表现力,应该使用std::transform
:
std::transform(begin(group), end(group), std::back_inserter(weights),
[](Person const& person){ return person.getWeight();});
什么时候不应该使用std::accumulate
?当std::accumulate
的返回值被丢弃时,表明该工具不适合使用。
四、std::accumulate 的进一步使用
使用std::accumulate()
可以实现STL中的几乎所有算法!此外,accumulate
还可以用来实现与std::all_of
功能等效的代码,但不会出现短路的情况。
std::accumulate(std::begin(booleans), std::end(booleans), true, std::logical_and<>())
还有更多。慢慢摸索…
五、总结
std::accumulate()
是一把有力的算法函数,但使用时需要明确它的原理和适用场景。它擅长于将一个范围汇总为单一值,但不擅长于对每个元素进行函数应用并收集结果。使用时要时刻谨记这一原则,以确保代码清晰易懂,体现算法的本意。