1. Введение
Если честно, программисты любят превращать всё в числа. Даже когда это не числа. У вас есть список оценок — хотите сумму. Есть список задач — хотите «сколько времени займёт всё вместе». Есть список строк — хотите собрать одну строку отчёта. Во всех этих случаях логика похожа: мы берём элементы по очереди и накапливаем один общий результат.
Эта идея называется свёртка (reduction / fold). Вручную вы делали это много раз: заводили переменную sum = 0, пробегались циклом и добавляли. std::accumulate — это стандартный, читабельный и довольно безопасный способ выразить такую операцию без «ритуальных» переменных и лишнего шума. Он не магический: внутри всё равно обычный цикл, просто аккуратно упакованный.
Чтобы не воспринимать accumulate как заклинание, держите в голове очень простую модель:
flowchart LR
A[init] --> B{берём следующий элемент}
B --> C["обновляем накопитель: acc = op(acc, x)"]
C --> B
B --> D[конец диапазона]
D --> E[возвращаем acc]
2. Подключение <numeric> и сигнатура std::accumulate
Сейчас будет важный «бытовой» момент: многие алгоритмы живут в <algorithm>, но std::accumulate — в другом заголовке. И это один из самых частых «почему оно не компилируется?!» у новичков. Поэтому запоминаем: accumulate находится в <numeric>.
Базовая форма выглядит так:
std::accumulate(first, last, init)
Она проходит по диапазону [first, last) и складывает элементы, начиная с init. Есть и расширенная форма с вашей операцией:
std::accumulate(first, last, init, op)
Где op — функция, которая говорит, как именно накапливать результат.
Мини‑пример: сумма целых чисел
#include <iostream>
#include <numeric>
#include <vector>
int main() {
std::vector<int> v{1, 2, 3, 4};
int sum = std::accumulate(v.begin(), v.end(), 0);
std::cout << sum << '\n'; // 10
}
Здесь важно, что 0 — это стартовое значение. Если бы вектор был пустым, результат всё равно был бы 0 (и это очень полезное поведение).
Почему init влияет на тип результата
Когда новички впервые слышат «init влияет на тип», обычно возникает реакция «да ладно, это же просто старт». Но в C++ тип результата действительно завязан на init, потому что накопитель (accumulator) начинается именно с него. Если вы стартуете с 0 (это int), то и копить вы, скорее всего, будете в int. Если стартуете с 0LL, то накопитель будет long long.
Это особенно важно, когда сумма может не влезть в int. И да, это та самая «школьная» проблема переполнения, которая в реальном коде внезапно превращается в баг «у нас общий бюджет стал отрицательным, но зато красиво».
Мини‑пример: переполнение int vs аккуратный long long
#include <iostream>
#include <numeric>
#include <vector>
int main() {
std::vector<int> prices{1'000'000'000, 1'000'000'000, 1'000'000'000};
long long total = std::accumulate(prices.begin(), prices.end(), 0LL);
std::cout << total << '\n'; // 3000000000
}
Если бы мы написали 0 вместо 0LL, накопление пошло бы в int и результат мог бы «сломаться» (в зависимости от платформы и компилятора). Поэтому практическое правило простое: если есть шанс, что сумма большая — стартуйте с 0LL.
Табличка‑шпаргалка по init
| Что считаем | Типичный init | Почему именно он |
|---|---|---|
| Сумма int | |
нейтральный элемент сложения и тип int |
| Сумма «побольше» | |
нейтральный элемент, но тип long long |
| Сумма double | |
чтобы накопление шло в double |
| Произведение | |
нейтральный элемент умножения |
| Склейка строк | |
пустая строка как нейтральный элемент |
3. Пользовательская операция: считаем по полю struct
До этого мы пользовались «умолчанием»: accumulate складывал элементы. Но на самом деле accumulate умеет почти любой «разумный» агрегат, если вы скажете, как обновлять накопитель. Для новичков хороший переходный мост — суммировать не сами элементы, а поле внутри struct.
Представим, что у нас есть задачи с оценкой времени. Мы продолжим наше условное учебное приложение «мини‑трекер задач».
Модель данных
#include <string>
struct Task {
int id{};
std::string title;
int minutes{}; // оценка времени на задачу
bool done{};
};
Теперь хотим посчитать «сколько минут у всех задач вместе». Вектор задач — это std::vector<Task>, а результат — число. Значит, операция должна уметь: взять acc (число) и Task, вернуть обновлённое число.
Мини‑пример: суммируем время задач через функцию
#include <numeric>
#include <vector>
int add_minutes(int acc, const Task& t) {
return acc + t.minutes;
}
А вот использование:
#include <iostream>
#include <numeric>
#include <vector>
int main() {
std::vector<Task> tasks{{1, "Read", 30, true}, {2, "Code", 50, false}};
int total = std::accumulate(tasks.begin(), tasks.end(), 0, add_minutes);
std::cout << total << '\n'; // 80
}
Обратите внимание на порядок параметров: функция для accumulate мыслится как op(accumulator, element), то есть «обнови накопитель по очередному элементу».
4. Другие агрегаты: произведение, конкатенация, счётчик
Очень легко застрять в идее «accumulate = сумма». Но правильнее думать так: это способ свести последовательность к одному значению. Иногда это сумма. Иногда — произведение. Иногда — строка отчёта. Иногда — даже «посчитать, сколько выполнено».
Произведение: нейтральный элемент — 1
Если вы стартуете произведение с 0, вы получите ноль навсегда (потому что 0 * что угодно = 0). Поэтому старт — 1.
#include <iostream>
#include <numeric>
#include <vector>
int mul(int acc, int x) {
return acc * x;
}
int main() {
std::vector<int> v{2, 3, 4};
int product = std::accumulate(v.begin(), v.end(), 1, mul);
std::cout << product << '\n'; // 24
}
Конкатенация строк: накапливаем std::string
Да, accumulate умеет собирать строку. Но тут спрятана тонкость: частая конкатенация может быть дорогой (перевыделения памяти). Мы пока не будем углубляться в оптимизацию, но честно скажем: для маленьких данных — нормально, для огромных — надо думать.
#include <iostream>
#include <numeric>
#include <string>
#include <vector>
std::string join_with_space(std::string acc, const std::string& s) {
if (!acc.empty()) acc += ' ';
acc += s;
return acc;
}
int main() {
std::vector<std::string> words{"C++", "is", "fine"};
std::string line = std::accumulate(words.begin(), words.end(), std::string{}, join_with_space);
std::cout << line << '\n'; // C++ is fine
}
Здесь функция принимает acc по значению, потому что она его всё равно меняет и возвращает. Для новичка это хороший компромисс по понятности: «берём текущее накопленное, добавляем кусочек, возвращаем».
Счётчик выполненных задач через accumulate
Формально, для этого есть count_if, и он чаще выглядит естественнее. Но как упражнение на понимание свёртки — отличный пример: ваш накопитель будет числом, а элемент — задача.
#include <numeric>
#include <vector>
int add_done_count(int acc, const Task& t) {
return acc + (t.done ? 1 : 0);
}
Использование:
#include <iostream>
#include <numeric>
#include <vector>
int main() {
std::vector<Task> tasks{{1, "Read", 30, true}, {2, "Code", 50, false}, {3, "Sleep", 10, true}};
int done_cnt = std::accumulate(tasks.begin(), tasks.end(), 0, add_done_count);
std::cout << done_cnt << '\n'; // 2
}
5. Используем accumulate в мини‑приложении задач
Сейчас мы сделаем самый практичный шаг: используем accumulate не как отдельный «демо‑трюк», а как часть логики приложения. Представьте, что у нас есть команда «stats», которая печатает краткую статистику: сколько задач всего, сколько выполнено и сколько минут суммарно. Количество задач — это tasks.size(), но выполненные и минуты — уже агрегаты.
Чтобы не превращать main в простыню, вынесем агрегаты в маленькие функции. Это соответствует идее «тонкий main» из ранних лекций: проще читать, проще тестировать глазами, проще менять.
Мини‑пример: функция «всего минут»
#include <numeric>
#include <vector>
int total_minutes(const std::vector<Task>& tasks) {
return std::accumulate(tasks.begin(), tasks.end(), 0, add_minutes);
}
Мини‑пример: функция «всего выполненных»
#include <numeric>
#include <vector>
int done_count(const std::vector<Task>& tasks) {
return std::accumulate(tasks.begin(), tasks.end(), 0, add_done_count);
}
Мини‑пример: печать статистики
#include <iostream>
#include <vector>
void print_stats(const std::vector<Task>& tasks) {
std::cout << "tasks: " << tasks.size() << '\n'; // tasks: 3
std::cout << "done: " << done_count(tasks) << '\n'; // done: 2
std::cout << "minutes total: " << total_minutes(tasks) << '\n'; // minutes total: 90
}
Мини‑пример: небольшой main, который показывает stats
#include <iostream>
#include <vector>
int main() {
std::vector<Task> tasks{{1, "Read", 30, true}, {2, "Code", 50, false}, {3, "Sleep", 10, true}};
print_stats(tasks);
}
Заметьте, как main стал почти «декларативным»: «вот данные, вот статистика». Логика подсчётов живёт отдельно — и это хороший стиль.
Пустой диапазон: что вернётся и почему это удобно
В реальной программе часто бывают пустые списки. Пользователь ещё не добавил задач, файл ещё не загрузился, фильтр всё отсеял. И вот тут проявляется очень приятное свойство: std::accumulate на пустом диапазоне возвращает init.
Это означает, что если вы правильно выбрали нейтральный элемент, то у вас получается «безопасная математика» без if (empty()) .... Например, сумма пустого списка — 0, произведение пустого списка — 1, склейка строк — пустая строка. Не надо писать лишние проверки, если поведение и так корректное.
Для начинающего это прям подарок: меньше ветвлений — меньше ошибок.
6. Типичные ошибки при работе с std::accumulate
Ошибка №1: забыли #include <numeric>.
Компилятор в таком случае может ругаться так, будто вы пытались вызвать несуществующую функцию, хотя вы уверены, что она есть. Лечится просто: accumulate живёт в <numeric>, а не в <algorithm>. Если вы подключили только <algorithm>, то std::accumulate может быть «не найден».
Ошибка №2: неверный init ломает смысл результата.
Самый классический пример — произведение со стартом 0. Код компилируется, программа работает, но результат всегда 0, и вы начинаете подозревать в злодействе математику. Тут важно не путать «старт» и «нейтральный элемент»: для умножения нейтральный элемент — 1, для сложения — 0.
Ошибка №3: не контролируют тип накопления и получают переполнение.
Если вы суммируете большие числа и стартуете с 0 (тип int), то вы сами себе подписываете контракт «считай в int». Правильнее стартовать с 0LL, если результат может быть большим. Это не «оптимизация», это корректность.
Ошибка №4: операция op написана с неправильным порядком аргументов.
Иногда пишут функцию в духе op(element, acc) по привычке, а accumulate ожидает op(acc, element). В результате можно получить странные ошибки компиляции или странную логику, если типы совпали случайно. Держите в голове модель: «первый аргумент — накопитель, второй — текущий элемент».
Ошибка №5: пытаются сделать в op «побочные эффекты» (печать, изменение контейнера).
Технически вы можете печатать внутри op, но тогда у вас свёртка превращается в «цикл с эффектами», который хуже читается и сложнее отлаживается. К тому же операции свёртки лучше мыслить как чистую функцию: вход → выход, без сюрпризов. Если хочется печатать — печатайте снаружи, отдельным кодом.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ