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, тому що накопичувач починається саме з нього. Якщо ви стартуєте з 0 (це int), то й накопичення, найімовірніше, відбуватиметься в int. Якщо ж стартуєте з 0LL, то накопичувач буде типу long long.
Це особливо важливо, коли сума може не вміститися в int. І так, це та сама «шкільна» проблема переповнення, яка в реальному коді раптом обертається на баг: «у нас загальний бюджет став відʼємним, зате красиво».
Мініприклад: переповнення int і акуратний 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 можна, але тоді згортка перетворюється на «цикл із побічними ефектами», який гірше читається і складніше налагоджується. До того ж операцію згортки краще уявляти як чисту функцію: вхід → вихід, без сюрпризів. Якщо хочеться друкувати — друкуйте зовні, окремим кодом.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ