Книга: Эффективное использование STL
Совет 37. Используйте accumulate или for_each для обобщения интервальных данных
Совет 37. Используйте accumulate или for_each для обобщения интервальных данных
Иногда возникает необходимость свести целый интервал к одному числу или, в более общем случае, к одному объекту. Для стандартных задач обобщения существуют специальные алгоритмы. Так, алгоритм count
возвращает количество элементов в интервале, а алгоритм count_if
возвращает количество элементов, соответствующих заданному предикату. Минимальное и максимальное значение элемента в интервале можно получить при помощи алгоритмов min_element
и max_element
.
Но в некоторых ситуациях возникает необходимость обработки интервальных данных по нестандартным критериям, и в таких случаях нужны более гибкие и универсальные средства, нежели алгоритмы count
, count_if
, min_element
и max_element
. Предположим, вы хотите вычислить сумму длин строк в контейнере, произведение чисел из заданного интервала, усредненные координаты точек и т. д. В каждом из этих случаев производится обобщение интервала, но при этом критерий обобщения вы должны определять самостоятельно. Для подобных ситуаций в STL предусмотрен специальный алгоритм accumulate
. Многим программистам этот алгоритм незнаком, поскольку в отличие от большинства алгоритмов он не находится в <algorthm>
, а вместе с тремя другими «числовыми алгоритмами» (inner_product
, adjacent_difference
и partial_sum
) выделен в библиотеку <numeric>
.
Как и многие другие алгоритмы, accumulate
существует в двух формах. Первая форма, получающая пару итераторов и начальное значение, возвращает начальное значение в сумме со значениями из интервала, определяемого итераторами:
list<double> ld; // Создать список и заполнить
// несколькими значениями типа double.
double sum = accumulate(ld.begin(), ld.end(), 0.0); // Вычислить сумму чисел
// с начальным значением 0.0
Обратите внимание: в приведенном примере начальное значение задается в форме 0.0. Эта подробность важна. Число 0.0 относится к типу double
, поэтому accumulate
использует для хранения вычисляемой суммы переменную типа double
. Предположим, вызов выглядит следующим образом:
double sum = accumulate(ld.begin(), ld.end(), 0); // Вычисление суммы чисел
// с начальным значением 0;
// неправильно!
В качестве начального значения используется int 0, поэтому accumulate накапливает вычисляемое значение в переменной типа int
. В итоге это значение будет возвращено алгоритмом accumulate
и использовано для инициализации переменной sum
. Программа компилируется и работает, но значение sum
будет неправильным. Вместо настоящей суммы списка чисел типа double переменная содержит сумму всех чисел, преобразуемую к int
после каждого суммирования.
Алгоритм accumulate
работает только с итераторами ввода и поэтому может использоваться даже с istream_iterator
и istreambuf_iterator
(см. совет 29):
cout << "The sum of the ints on the standard input is " // Вывести сумму
<< accumulate(istream_iterator<int>(cin), // чисел из входного
istream_iterator<int>(), // потока
0);
Из-за своей первой, стандартной формы алгоритм accumulate
был отнесен к числовым алгоритмам. Но существует и другая, альтернативная форма, которой при вызове передается начальное значение и произвольная обобщающая функция. В этом варианте алгоритм accumulate
становится гораздо более универсальным.
В качестве примера рассмотрим возможность применения accumulate
для вычисления суммы длин всех строк в контейнере. Для вычисления суммы алгоритм должен знать две вещи: начальное значение суммы (в данном случае 0) и функцию обновления суммы для каждой новой строки. Следующая функция берет предыдущее значение суммы, прибавляет к нему длину новой строки и возвращает обновленную сумму:
string::size_type // См. далее
stringLengthSum(string::size_type sumSoFar, const string& s) {
return sumSoFar + s.size();
}
Тело функции убеждает в том, что происходящее весьма тривиально, но на первый взгляд смущают объявления string::size_type
. На самом деле в них нет ничего страшного. У каждого стандартного контейнера STL имеется определение типа size_type
, относящееся к счетному типу данного контейнера. В частности, значение этого типа возвращается функцией size. Для всех стандартных контейнеров определение size_type
должно совпадать с size_t
, хотя теоретически нестандартные STL-совместимые контейнеры могут использовать в size_type
другой тип (хотя я не представляю, для чего это может понадобиться). Для стандартных контейнеров запись контейнер::size_type
можно рассматривать как специальный синтаксис для size_t
.
Функция stringLenghSum
является типичным представителем обобщающих функций, используемых при вызове accumulate
. Функция получает текущее значение суммы и следующий элемент интервала, а возвращает новое значение накапливаемой суммы. Накапливаемая сумма (сумма длин строк, встречавшихся ранее) относится к типу string::size_type
, а обрабатываемый элемент относится к типу string
. Как это часто бывает, возвращаемое значение относится к тому же типу, что и первый параметр функции.
Функция stringLenghSum
используется в сочетании с accumulate
следующим образом:
set<string> ss; // Создать контейнер строк
… // и заполнить его данными
string::size_type lengthSum = // Присвоить lengthSum
accumulate(ss.begin(), ss.end(), // результат вызова stringLengthSum
0, stringLengthSum); // для каждого элемента ss
// с нулевым начальным значением
Изящно, не правда ли? Произведение вычисляется еще проще, поскольку вместо написания собственной функции суммирования можно обойтись стандартным функтором multiplies
:
vector<float> vf; // Создать контейнер типа float
… // и заполнить его данными
float product = // Присвоить product результат
accumulate(vf.begin(), vf.end(), // вызова multiplies<float>
1.0, multples<float>()); // для каждого элемента vf
// с начальным значением 1.0
Только не забудьте о том, что начальное значение вместо нуля должно быть равно единице (в вещественном формате, не в int
!). Если начальное значение равно нулю, то результат всегда будет равен нулю — ноль, умноженный на любое число, остается нулем.
Последний пример не столь тривиален. В нем вычисляется среднее арифметическое по интервалу точек, представленных структурами следующего вида:
struct Point {
Point(double initX, double initY): x(initX), y(initY) {}
double x, y;
};
В этом примере обобщающей функцией будет функтор PointAverage
, но перед рассмотрением класса этого функтора стоит рассмотреть его использование при вызове accumulate
:
list<Point> lp;
…
Point avg = // Вычисление среднего
accumulate(lp.begin(), lp.end(), // арифметического по точкам,
Point(0,0), PointAverage()); // входящим в список lр
Просто и бесхитростно, как и должно быть. На этот раз в качестве начального значения используется объект
Point, соответствующий началу координат, а нам остается лишь помнить о необходимости исключения этой точки из вычислений.
Функтор PointAverage
отслеживает количество обработанных точек, а также суммы их компонентов x
и y
. При каждом вызове он обновляет данные и возвращает средние координаты по обработанным точкам. Поскольку для каждой точки в интервале функтор вызывается ровно один раз, он делит суммы по составляющим x
и y
на количество точек в интервале. Начальная точка, переданная при вызове accumulate
, игнорируется.
class PointAverage:
publiс binary_function<Point, Point, Point> {
public:
PointAverage(): xSum(0), ySum(0), numPoints(0) {}
const Point operator() (const Point& avgSoFar, const Point& p) {
++numPoints;
xSum += p.x;
ySum += p.y;
return Point(xSum/numPoints, ySum/numPoints);
}
private:
size_t numPoints;
double xSum;
double ySum;
};
Такое решение прекрасно работает, и лишь из-за периодических контактов с неординарно мыслящими личностями (многие из которых работают в Комитете по стандартизации) я могу представить себе реализации STL, в которых возможны проблемы. Тем не менее, PointAverage
нарушает параграф 2 раздела 26.4.1 Стандарта, который, как вы помните, запрещает побочные эффекты по отношению к функции,передаваемой accumulate
. Модификация переменных numPoints
, xSum
и ySum
относится к побочным эффектам, поэтому с технической точки зрения приведенный выше фрагмент приводит к непредсказуемым последствиям. На практике трудно представить, что приведенный код может не работать, но чтобы моя совесть была чиста, я обязан специально оговорить это обстоятельство.
Впрочем, у меня появляется удобная возможность упомянуть о for_each
— другом алгоритме, который может использоваться для обобщения интервалов. На for_each
не распространяются ограничения, установленные для accumulate
. Алгоритм for_each
, как и accumulate
, получает интервал и функцию (обычно в виде объекта функции), вызываемую для каждого элемента в интервале, однако функция, передаваемая for_each
, получает только один аргумент (текущий элемент интервала), а после завершения работы for_each
возвращает свою функцию (а точнее, ее копию — см. совет 38). Что еще важнее, переданная (и позднее возвращаемая) функция может обладать побочными эффектами.
Помимо побочных эффектов между for_each
и accumulate
существуют два основных различия. Во-первых, само название accumulate
ассоциируется с вычислением сводного значения по интервалу, а название for_each
скорее предполагает выполнение некой операции с каждым элементом интервала. Алгоритм for_each
может использоваться дя вычисления сводной величины, но такие решения по наглядности уступают accumulate
.
Во-вторых, accumulate непосредственно возвращает вычисленное значение, а for_each
возвращает объект функции, используемый для дальнейшего получения информации. В C++ это означает, что в класс функтора необходимо включить функцию для получения искомых данных.
Ниже приведен предыдущий пример, в котором вместо accumulate используется for_each
:
struct Point{…}; // См. ранее
class PointAverage;
public unary_function<Point, void>{ // См. совет 40
public:
PointAverage(): xSum(0), ySum(0), numPoints(0) {}
void operator()(const Point& p) {
++numPoints;
xSum += p.x;
ySum += p.y;
}
Point result() const {
return Point(xSum/numPoints, ySum/numPoints);
}
private:
size t numPoints;
double xSum;
double ySum;
};
list<Point> lp;
…
Point avg = for_each(lp.begin(), lp.end(), PointAverage()).result();
Лично я предпочитаю обобщать интервальные данные при помощи accumulate
, поскольку мне кажется, что этот алгоритм наиболее четко передает суть происходящего, однако foreach
тоже работает, а вопрос побочных эффектов для for_each
не так принципиален, как для accumulate
. Словом, для обобщения интервальных данных могут использоваться оба алгоритма; выберите тот, который вам лучше подойдет.
Возможно, вас интересует, почему у for_each
параметр-функция может иметь побочные эффекты, а у accumulate
— не может? Представьте, я бы тоже хотел это знать. Что ж, дорогой читатель, некоторые тайны остаются за пределами наших познаний. Чем accumulate
принципиально отличается от for_each
? Пока я еще не слышал убедительного ответа на этот вопрос.
- Совет 30. Следите за тем, чтобы приемный интервал имел достаточный размер
- Совет 31. Помните о существовании разных средств сортировки
- Совет 32. Сопровождайте вызовы remove-подобных алгоритмов вызовом erase
- Совет 33. Будьте внимательны при использовании remove-подобных алгоритмов с контейнерами указателей
- Совет 34. Помните о том. какие алгоритмы получают сортированные интервалы
- Совет 35. Реализуйте простые сравнения строк без учета регистра символов с использованием mismatch или lexicographical_compare
- Совет 36. Правильно реализуйте copy_if
- Совет 37. Используйте accumulate или for_each для обобщения интервальных данных
- Резервное копирование базы данных InterBase
- Firebird РУКОВОДСТВО РАЗРАБОТЧИКА БАЗ ДАННЫХ
- Резервное копирование многофайловых баз данных
- Восстановление из резервных копий многофайловых баз данных
- Владелец базы данных
- ЧАСТЬ IV. База данных и ее объекты.
- Перевод базы данных InterBase 6.x на 3-й диалект
- Типы данных для работы с датой и временем
- Практическая работа 53. Запуск Access. Работа с объектами базы данных
- Обзор основных причин повреждения базы данных
- Forced writes - палка о двух концах
- Ошибки проектирования базы данных