Функция std::accumulate из заголовка <numeric>  —  это универсальный инструмент для выполнения операций с диапазоном элементов.

В простейшем виде ею суммируется диапазон чисел:

#include <iostream>
#include <vector>
#include <numeric>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
int sum = std::accumulate(numbers.begin(), numbers.end(), 0);
std::cout << "Sum: " << sum << std::endl; // Вывод: «Сумма: 15»
return 0;
}

Здесь 0  —  начальное значение, каждый элемент к нему добавляется этой функцией.

Вычисление различных типов

std::accumulate не ограничивается целыми числами, работает с любым типом, которым поддерживается сложение:

#include <iostream>
#include <vector>
#include <numeric>
#include <string>

int main() {
std::vector<std::string> words = {"Hello", " ", "World", "!"};
std::string result = std::accumulate(words.begin(), words.end(), std::string(""));
std::cout << "Result: " << result << std::endl; // Вывод: «Результат: Hello World!»
return 0;
}

Здесь начальное значение  —  std::string(“”), используемое для конкатенации строк.

Пользовательские бинарные операции

Реальная гибкость std::accumulate обнаруживается в применении пользовательских бинарных операций:

#include <iostream>
#include <vector>
#include <numeric>

int multiply(int a, int b) {
return a * b;
}

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
int product = std::accumulate(numbers.begin(), numbers.end(), 1, multiply);
std::cout << "Product: " << product << std::endl; // Вывод: «Произведение: 120»
return 0;
}

Здесь accumulate использована для вычисления произведения всех элементов.

Лямбда-функции с accumulate

С лямбда-функциями код лаконичнее:

#include <iostream>
#include <vector>
#include <numeric>

int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
int sum_of_squares = std::accumulate(numbers.begin(), numbers.end(), 0,
[](int total, int num) { return total + num * num; });
std::cout << "Sum of squares: " << sum_of_squares << std::endl; // Вывод: «Сумма квадратов: 55»
return 0;
}

В этом примере вычисляется сумма квадратов всех элементов.

Вычисление со сложными объектами

std::accumulate работает и с объектами посложнее:

#include <iostream>
#include <vector>
#include <numeric>
#include <string>

struct Person {
std::string name;
int age;
};

int main() {
std::vector<Person> people = {
{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}
};

int total_age = std::accumulate(people.begin(), people.end(), 0,
[](int sum, const Person& p) { return sum + p.age; });

std::cout << "Total age: " << total_age << std::endl; // Вывод: «Общий возраст: 90»
return 0;
}

Здесь суммируется возраст всех объектов Person в векторе.

Реальный сценарий: финансовые расчеты

Воспользуемся std::accumulate для более практичного сценария  —  вычисления сложных процентов:

#include <iostream>
#include <vector>
#include <numeric>
#include <cmath>
#include <iomanip>

struct YearlyInterest {
double principal;
double rate;
};

int main() {
std::vector<YearlyInterest> investments = {
{1000, 0.05},
{2000, 0.03},
{1500, 0.04},
{3000, 0.035}
};

double total = std::accumulate(investments.begin(), investments.end(), 0.0,
[](double sum, const YearlyInterest& inv) {
return sum + inv.principal * (1 + inv.rate);
});

std::cout << "Total after one year: $"
<< std::fixed << std::setprecision(2) << total << std::endl;
// Вывод: «Итог после одного года: 7780,00 $»

return 0;
}

В этом примере рассчитывается общее сумма инвестиций после одного года начисления сложных процентов.

accumulate для нечисловых операций

std::accumulate применяется не только для чисел, им выполняется любая ассоциативная операция:

#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>

int main() {
std::vector<std::vector<int>> nested = {{1, 2}, {3, 4}, {5, 6}};

std::vector<int> flattened = std::accumulate(nested.begin(), nested.end(),
std::vector<int>{},
[](std::vector<int> acc, const std::vector<int>& vec) {
acc.insert(acc.end(), vec.begin(), vec.end());
return acc;
});

for (int num : flattened) {
std::cout << num << " ";
}
std::cout << std::endl;
// Вывод: 1 2 3 4 5 6

return 0;
}

В этом примере accumulate используется для сглаживания вложенного вектора.

Параллельное вычисление с C++17

В C++17 появились параллельные алгоритмы, в том числе std::reduce, который отличается от accumulate параллельным выполнением:

#include <iostream>
#include <vector>
#include <numeric>
#include <execution>

int main() {
std::vector<int> numbers(10000000, 1); // 10 миллионов 1 сек.

auto sum = std::reduce(std::execution::par, numbers.begin(), numbers.end());
std::cout << "Sum: " << sum << std::endl; // Вывод: «Сумма: 10000000»

return 0;
}

В многоядерных системах этим сильно ускоряется вычисление больших наборов данных.

Производительность

Хотя std::accumulate универсальна, наиболее эффективным вариантом она является не всегда. Ручным циклом простые операции над базовыми типами выполняются быстрее:

#include <iostream>
#include <vector>
#include <numeric>
#include <chrono>

int main() {
std::vector<int> numbers(10000000, 1); // 10 миллионов 1 сек.

auto start = std::chrono::high_resolution_clock::now();
int sum1 = std::accumulate(numbers.begin(), numbers.end(), 0);
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> diff1 = end - start;

start = std::chrono::high_resolution_clock::now();
int sum2 = 0;
for (int num : numbers) {
sum2 += num;
}
end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> diff2 = end - start;

std::cout << "accumulate time: " << diff1.count() << " seconds\n";
std::cout << "manual loop time: " << diff2.count() << " seconds\n";

return 0;
}

Запустите этот тест у себя в системе и сравните производительность.

accumulate и числа с плавающей точкой

Будьте осторожны, используя accumulate с числами с плавающей точкой, ибо последовательность операций сказывается на результате из-за ошибок округления:

#include <iostream>
#include <vector>
#include <numeric>
#include <iomanip>

int main() {
std::vector<double> values = {0.1, 0.2, 0.3, 0.4, 0.5};

double sum1 = std::accumulate(values.begin(), values.end(), 0.0);
double sum2 = std::accumulate(values.rbegin(), values.rend(), 0.0);

std::cout << std::setprecision(20);
std::cout << "Forward sum: " << sum1 << std::endl;
std::cout << "Reverse sum: " << sum2 << std::endl;

return 0;
}

Из-за очередности вычислений результаты sum1 и sum2 могут немного отличаться.

Типичные ошибки и как их избежать

  1. Упускается из виду начальное значение. Всегда указывайте его, для сумм используйте 0, для произведений  —  1.
  2. Некорректный тип начального значения. Убедитесь, что он соответствует предполагаемой операции:
std::vector<double> values = {1.1, 2.2, 3.3};
double sum = std::accumulate(values.begin(), values.end(), 0); // Некорректный: 0 - целое число
double correctSum = std::accumulate(values.begin(), values.end(), 0.0); // Корректный

3. Изменение аккумулятора сумм на месте. При применении пользовательских функций не забывайте, что аккумулятор передается по значению. Меняйте и возвращайте его, не пытайтесь изменить на месте.

4. Упускается из виду возможное переполнение. Для аккумулятора с большими суммами используйте тип побольше:

std::vector<int> largeNumbers(1000000, 1000);
long long sum = std::accumulate(largeNumbers.begin(), largeNumbers.end(), 0LL);

5. Подразумевается ассоциативность, а операции с плавающей точкой не строго ассоциативны. Что касается чисел с плавающей точкой, результаты поточне́е получайте применением std::accumulate с пользовательской функцией, которой реализуется более надежный алгоритм суммирования.

Заключение

Функция std::accumulate на C++ не ограничивается простым суммированием. Ею лаконично выполняются самые разные операции над последовательностями данных  —  от конкатенации строк до вычислений со сложными объектами.

При работе с accumulate учитывается характер данных и операций. Это отличный инструмент, но оптимален он не всегда и не в любой ситуации.

С простыми операциями над базовыми типами ручной цикл справляется эффективнее. Для больших наборов данных, где приходится кстати параллелизм, применяется std::reduce.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи ryan: Detailed Guide on Std::accumulate in C++

Предыдущая статьяЕсли вы застряли между этажами: как алгоритм лифта заставляет нас бесконечно ждать
Следующая статьяКак спроектировать рекомендательную систему