YAOTU INSIGHTS

C++的STL中accumulate函数的使用方法

C++的STL中accumulate函数的使用方法
前言std::accumulate定义在numeric中是 STL 里最常用的折叠fold算法给定一个区间和一个初值从左到右依次把元素“累加”进初值。它的名字容易让人以为只能做加法实际上第四个参数可以传入任意二元操作求和、求积、求最值、拼接字符串都能用它完成。围绕accumulate的误解主要集中在三点。第一初值的类型决定了整个计算过程的类型用0去累加double会在每一步截断第二空区间的返回值就是初值不是 0第三std::accumulate是顺序执行的左折叠不能指望它自动并行C17 的std::reduce才是为并行设计的但对操作的结合律和交换律有要求。本文从函数签名和折叠顺序讲起再讲初值类型、自定义操作、字符串拼接和最值统计最后对比accumulate与reduce。代码以 C17 为基准涉及 C20 的地方会注明。文中不声称代码经过编译或运行结果均按标准语义描述。一、函数签名与折叠顺序std::accumulate有两个重载// 适用于 C17以下为声明示意template class InputIt, class TT accumulate(InputIt first, InputIt last, T init);template class InputIt, class T, class BinaryOperationT accumulate(InputIt first, InputIt last, T init, BinaryOperation op);第一个重载用把元素累加到init上第二个重载用op替代。返回类型都是T也就是初值的类型可能经过推导而不是元素类型。标准规定的计算顺序是严格的左折叠result initresult result *firstresult result *(first 1)...用自定义操作表示就是op(op(op(init, a1), a2), a3)。顺序是从左到右不会重排也不会并行。复杂度是恰好last - first次操作空区间时一次都不执行直接返回init。// 适用于 C17#include iostream#include numeric#include vectorint main() {std::vectorint values{1, 2, 3, 4};int sum std::accumulate(values.begin(), values.end(), 0);int product std::accumulate(values.begin(), values.end(), 1,[](int acc, int value) {return acc * value;});std::cout sum \n; // 10std::cout product \n; // 24return 0;}这里的注释是计算语义上的结果不是某次运行的输出。accumulate对迭代器类别的要求是输入迭代器InputIterator所以链表、输入流迭代器也能用但它只能单遍从左到右处理。二、初值类型决定一切这是accumulate最容易出错的地方。标准里的累加表达式是init init *firstC20 起允许实现使用std::move(init)赋值的左操作数是T类型的init。因此初值是int每一步的中间结果都会转换回int元素是doubleint double先得到double再赋回int时发生截断最终返回的也是int小数部分全部丢失。// 适用于 C17#include numeric#include vectorint main() {std::vectordouble prices{1.5, 2.5, 3.5};double correct std::accumulate(prices.begin(), prices.end(), 0.0);int wrong std::accumulate(prices.begin(), prices.end(), 0);// correct 为 7.5wrong 的每一步都被截断结果不是 7.5return (correct 7.0 wrong 0) ? 0 : 1;}要得到浮点结果初值必须写成0.0double或0.0ffloat。同样的道理累加很大的整数时初值要写0LL或std::int64_t{0}避免int溢出。有符号整数溢出是未定义行为undefined behavior, UB不是“回绕成负数”这么简单。如果元素类型和初值类型不同二元操作要能接受这两种类型。对于内置的算术转换会处理对于自定义 lambda参数类型要写清楚// 适用于 C17#include numeric#include vectorint main() {std::vectorint counts{1, 2, 3};long long total std::accumulate(counts.begin(), counts.end(), 0LL,[](long long acc, int value) {return acc static_castlong long(value);});return total 6 ? 0 : 1;}0LL让T成为long longlambda 的第一个参数也相应写成long long整条计算链都不会退回int。三、自定义二元操作求和、求积与最值标准库在functional里提供了std::plus、std::multiplies等函数对象可以直接作为第四个参数// 适用于 C17#include functional#include numeric#include vectorint main() {std::vectorint values{2, 3, 4};int sum std::accumulate(values.begin(), values.end(), 0, std::plusint());int product std::accumulate(values.begin(), values.end(), 1, std::multipliesint());return (sum 9 product 24) ? 0 : 1;}求最大值时初值不能随便写 0如果所有元素都是负数0 会错误地成为最大值。安全的做法是用元素类型的下限或者先判断区间非空// 适用于 C17#include algorithm#include limits#include numeric#include vectorint main() {std::vectorint values{-5, -2, -9};int maximum std::accumulate(values.begin(), values.end(),std::numeric_limitsint::min(),[](int acc, int value) { return std::max(acc, value); });return maximum -2 ? 0 : 1;}也可以把初值设为第一个元素但从第二个元素开始迭代避免空区间解引用// 适用于 C17#include algorithm#include numeric#include vectorint main() {std::vectorint values{-5, -2, -9};if (values.empty()) {return 1;}int maximum std::accumulate(values.begin() 1, values.end(), values.front(),[](int acc, int value) { return std::max(acc, value); });return maximum -2 ? 0 : 1;}字符串拼接把一组单词拼成句子是accumulate的经典用法但初值必须写成std::string{}不能写// 适用于 C17#include numeric#include string#include vectorint main() {std::vectorstd::string words{C, STL, accumulate};std::string sentence std::accumulate(words.begin(), words.end(), std::string{},[](std::string acc, const std::string word) {if (!acc.empty()) {acc ;}acc word;return acc;});return sentence C STL accumulate ? 0 : 1;}如果初值写T会被推导为const char*。const char* std::string虽然能产生std::string但这个结果要赋回const char*既不能编译也完全不符合“拼接字符串”的意图。写std::string{}明确告诉编译器累加器是字符串对象。lambda 的第一个参数按值接收std::string每次调用都会构造一个新的累加器副本。对短字符串通常可以接受如果拼接量很大可以考虑传入时用std::move的习惯写法或者改用循环加避免反复复制。accumulate的接口决定了它每次都要把累加器交给操作再取回无法完全避免这一层。逻辑运算的陷阱accumulate不会短路。用它做“所有元素都满足条件”的判断会把整个区间走完// 适用于 C17 —— 不推荐bool all_positive std::accumulate(values.begin(), values.end(), true,[](bool acc, int value) { return acc (value 0); });这段代码语义上能得到正确结果但没有短路遇到第一个不满足的元素后仍会继续遍历。标准库提供了带短路的算法std::all_of、std::any_of、std::none_of这类判断应当优先使用它们。四、accumulate 与 reduce 的区别C17 在numeric中加入了std::reduce它允许实现按任意顺序合并元素也可以接受执行策略并行执行// 需要 C17#include numeric#include vectorint main() {std::vectorint values{1, 2, 3, 4};int sum std::reduce(values.begin(), values.end(), 0);int product std::reduce(values.begin(), values.end(), 1,std::multipliesint());return (sum 10 product 24) ? 0 : 1;}std::reduce对操作的要求比accumulate高操作应当是结合的通常还应当是交换的否则重排会改变结果。整数加法和乘法满足结合律不考虑溢出浮点加法不满足——(a b) c与a (b c)可能得到不同的舍入结果。用reduce做浮点求和时结果可能与顺序累加不同这是标准允许的不是 bug。特性std::accumulatestd::reduce引入版本C98C20 起 constexprC17计算顺序严格从左到右未指定可重排是否支持执行策略否是对操作的要求任意二元操作结合律通常还需交换律空区间返回init返回init典型用途顺序折叠、字符串拼接、依赖顺序的计算可并行的求和、计数、聚合并行版本要包含execution并传入执行策略// 需要 C17并行算法支持视工具链而定#include execution#include numeric#include vectorint main() {std::vectorint values(1000, 1);int sum std::reduce(std::execution::par, values.begin(), values.end(), 0);return sum 1000 ? 0 : 1;}并行执行策略需要底层运行时支持GCC 通常要额外链接并行后端例如 TBBMSVC 和 libstdc 的配置方式不同。是否真的加速取决于数据规模、硬件线程数和任务划分不能仅凭“用了 par”就断定更快应当用实际负载测量并注意测量噪声。完整示例价格统计// 适用于 C17#include algorithm#include iostream#include numeric#include string#include vectorint main() {std::vectordouble prices{9.99, 19.50, 5.25, 30.00};const double total std::accumulate(prices.begin(), prices.end(), 0.0);const double average prices.empty()? 0.0: total / static_castdouble(prices.size());const double maximum std::accumulate(prices.begin(), prices.end(), 0.0,[](double acc, double value) { return std::max(acc, value); });std::vectorstd::string tags{sale, new, limited};const std::string label std::accumulate(tags.begin(), tags.end(), std::string{},[](std::string acc, const std::string tag) {if (!acc.empty()) {acc /;}acc tag;return acc;});std::cout total total \n;std::cout average average \n;std::cout maximum maximum \n;std::cout label label \n;return 0;}total用0.0作为初值整条计算链都是doubleaverage先判断空区间避免除以 0maximum的初值是0.0因为价格都是非负数如果有负数价格就应改用std::numeric_limitsdouble::lowest()label用std::string{}作为初值逐段拼接。常见坑点❌std::accumulate(values.begin(), values.end(), 0)而values是std::vectordouble。✅ 初值写0.0让累加器类型是double避免每一步截断。❌std::accumulate(words.begin(), words.end(), )拼接字符串。✅ 初值写std::string{}会让T推导成const char*。❌ 认为空区间返回 0或对空区间做除法求平均。✅ 空区间返回init求平均前先判断empty()。❌ 求最大值时把初值写成 0而区间全是负数。✅ 用std::numeric_limitsT::lowest()或用首元素作初值并从第二个元素开始。❌ 用int累加可能超出范围的数据指望溢出后回绕。✅ 用更宽的初值类型如0LL有符号整数溢出是 UB。❌ 用std::reduce执行依赖顺序的操作例如带状态的字符串拼接。✅ 顺序敏感用std::accumulatereduce只用于满足结合律、可重排的聚合。❌ 在自定义操作里修改容器元素或修改外部状态期望accumulate保证调用次数和顺序。✅ 操作应保持纯粹需要副作用时用显式循环语义更清楚。❌ 以为accumulate会自动并行或以为std::execution::par一定更快。✅accumulate是顺序算法并行用reduce加执行策略并用真实负载测量。总结要点结论头文件numeric计算顺序从左到右的左折叠严格顺序返回类型初值的类型T初值选择决定精度和溢出行为浮点用0.0字符串用std::string{}空区间返回init不执行任何操作自定义操作接收累加器和元素返回可转换回T的值并行用 C17 的std::reduce不是accumulate短路不支持逻辑判断用all_of/any_ofaccumulate的接口只有四个参数但初值类型、操作签名和空区间语义三处都容易出错。把它当成“带初值的有序折叠”而不是“只能求和的函数”很多用法就顺了求和只是这一种特例求积、求最值、拼接、计数都可以用同一个骨架表达。需要并行或顺序无关的聚合时再换std::reduce并且先确认操作满足结合律。