JavaRush /Курсы /C++ SELF /std::accumulate: суммирование и простые агрегаты

std::accumulate: суммирование и простые агрегаты

C++ SELF
21 уровень , 2 лекция
Открыта

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
0
нейтральный элемент сложения и тип int
Сумма «побольше»
0LL
нейтральный элемент, но тип long long
Сумма double
0.0
чтобы накопление шло в double
Произведение
1
нейтральный элемент умножения
Склейка строк
std::string{}
пустая строка как нейтральный элемент

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, но тогда у вас свёртка превращается в «цикл с эффектами», который хуже читается и сложнее отлаживается. К тому же операции свёртки лучше мыслить как чистую функцию: вход → выход, без сюрпризов. Если хочется печатать — печатайте снаружи, отдельным кодом.

1
Задача
C++ SELF, 21 уровень, 2 лекция
Недоступна
Сумма смены
Сумма смены
1
Задача
C++ SELF, 21 уровень, 2 лекция
Недоступна
Подсчёт успехов
Подсчёт успехов
1
Задача
C++ SELF, 21 уровень, 2 лекция
Недоступна
Итог корзины
Итог корзины
1
Задача
C++ SELF, 21 уровень, 2 лекция
Недоступна
Сборка CSV
Сборка CSV
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ