Понимание std :: accumulate

Я хочу знать, зачем нужен третий параметр std::accumulate (он же сокращение). Для тех, кто не знает, что такое accumulate, используется так:

vector<int> V{1,2,3};  
int sum = accumulate(V.begin(), V.end(), 0);
// sum == 6

Вызов accumulate эквивалентен:

sum = 0;  // 0 - value of 3rd param
for (auto x : V)  sum += x;

Также есть необязательный 4-й параметр, позволяющий заменить сложение любой другой операцией.

Обоснование, которое я слышал, заключается в том, что если вам нужно, скажем, не складывать, а умножать элементы вектора, нам нужно другое (ненулевое) начальное значение:

vector<int> V{1,2,3};
int product = accumulate(V.begin(), V.end(), 1, multiplies<int>());

Но почему бы не сделать как Python - установить начальное значение для V.begin() и использовать диапазон, начиная с V.begin()+1. Что-то вроде этого:

int sum = accumulate(V.begin()+1, V.end(), V.begin());

Это будет работать для любой операции. Зачем вообще нужен 3-й параметр?


person Leonid Volnitsky    schedule 28.09.2012    source источник
comment
accumulate(V.begin()+1, V.end(), V.begin()); не работает, если V пусто.   -  person verdesmarald    schedule 28.09.2012
comment
Другая причина заключается в том, что общая сумма больше не вписывается в тип, поэтому вам нужно указать другой тип, который может содержать значение.   -  person Jesse Good    schedule 28.09.2012


Ответы (5)


При такой ситуации раздражает код, который точно знает, что диапазон не пуст и который хочет начать накопление с первого элемента диапазона. В зависимости от операции, которая используется для накопления, не всегда очевидно, какое «нулевое» значение использовать.

С другой стороны, если вы предоставляете только версию, требующую непустых диапазонов, это раздражает вызывающих абонентов, которые не знают наверняка, что их диапазоны не пусты. На них ложится дополнительная нагрузка.

Одна точка зрения состоит в том, что лучшее из обоих миров - это, конечно, обеспечение обеих функций. Например, Haskell предоставляет как foldl1, так и foldr1 (которые требуют непустых списков) вместе с foldl и foldr (которые отражают std::transform).

Другая точка зрения заключается в том, что, поскольку одно может быть реализовано в терминах другого с помощью тривиального преобразования (как вы продемонстрировали: std::transform(std::next(b), e, *b, f) - std::next - это C ++ 11, но точка зрения все еще остается в силе), предпочтительно сделать интерфейс как минимальный, насколько это может быть без реальной потери выразительной силы.

person Luc Danton    schedule 28.09.2012

Вы делаете ошибочное предположение: этот тип T того же типа, что и InputIterator.

Но std::accumulate является общим и допускает всевозможные творческие накопления и сокращения.

Пример №1: Накопление заработной платы по всем сотрудникам

Вот простой пример: класс Employee с множеством полей данных.

class Employee {
/** All kinds of data: name, ID number, phone, email address... */
public:
 int monthlyPay() const;
};

Вы не можете осмысленно «накапливать» набор сотрудников. Это бессмысленно; это не определено. Но вы можете определить накопление в отношении сотрудников. Допустим, мы хотим просуммировать всю ежемесячную заработную плату всех сотрудников. std::accumulate может это сделать:

/** Simple class defining how to add a single Employee's
 *  monthly pay to our existing tally */
auto accumulate_func = [](int accumulator, const Employee& emp) {
   return accumulator + emp.monthlyPay();
 };

// And here's how you call the actual calculation:
int TotalMonthlyPayrollCost(const vector<Employee>& V)
{
 return std::accumulate(V.begin(), V.end(), 0, accumulate_func);
}

Итак, в этом примере мы накапливаем int значение в коллекции из Employee объектов. Здесь сумма накопления не того же типа переменной, по которой мы фактически суммируем.

Пример № 2: Накопление среднего

Вы также можете использовать accumulate для более сложных типов накоплений - возможно, захотите добавить значения к вектору; возможно, у вас есть какая-то загадочная статистика, которую вы отслеживаете по входным данным; и т. д. То, что вы накапливаете, не должно быть просто числом; это может быть что-то более сложное.

Например, вот простой пример использования accumulate для вычисления среднего значения вектора целых чисел:

// This time our accumulator isn't an int -- it's a structure that lets us
// accumulate an average.
struct average_accumulate_t
{
    int sum;
    size_t n;
    double GetAverage() const { return ((double)sum)/n; }
};

// Here's HOW we add a value to the average:
auto func_accumulate_average = 
    [](average_accumulate_t accAverage, int value) {
        return average_accumulate_t(
            {accAverage.sum+value, // value is added to the total sum
            accAverage.n+1});      // increment number of values seen
    };

double CalculateAverage(const vector<int>& V)
{
    average_accumulate_t res =
        std::accumulate(V.begin(), V.end(), average_accumulate_t({0,0}), func_accumulate_average)
    return res.GetAverage();
}

Пример № 3: Накопление скользящего среднего

Еще одна причина, по которой вам нужно начальное значение, заключается в том, что это значение не всегда является значением по умолчанию / нейтральным для вычислений, которые вы делаете.

Давайте возьмем средний пример, который мы уже видели. Но теперь нам нужен класс, который может удерживать текущее среднее значение, то есть мы можем продолжать вводить новые значения и проверять среднее на данный момент по нескольким вызовам. .

class RunningAverage
{
    average_accumulate_t _avg;
public:
    RunningAverage():_avg({0,0}){} // initialize to empty average

    double AverageSoFar() const { return _avg.GetAverage(); }

    void AddValues(const vector<int>& v)
    {
        _avg = std::accumulate(v.begin(), v.end(), 
            _avg, // NOT the default initial {0,0}!
            func_accumulate_average);
    }

};

int main()
{
    RunningAverage r;
    r.AddValues(vector<int>({1,1,1}));
    std::cout << "Running Average: " << r.AverageSoFar() << std::endl; // 1.0
    r.AddValues(vector<int>({-1,-1,-1}));
    std::cout << "Running Average: " << r.AverageSoFar() << std::endl; // 0.0
}

Это тот случай, когда мы полностью полагаемся на возможность установить это начальное значение для std::accumulate - нам нужно иметь возможность инициализировать накопление с разных начальных точек.


Таким образом, std::accumulate подходит для любого случая, когда вы выполняете итерацию по входному диапазону и создаете один единственный результат в этом диапазоне. Но результат не обязательно должен быть того же типа, что и диапазон, и вы не можете делать никаких предположений о том, какое начальное значение использовать - вот почему у вас должен быть начальный экземпляр для использования в качестве результата накопления.

person Ziv    schedule 18.02.2015
comment
Этот момент следует еще раз подчеркнуть. Буквально на днях я обнаружил неприятную ошибку, при которой я хотел выполнить float операцию с вектором, но поскольку я просто использовал 0 в качестве начального значения, компилятор вместо этого выполнил все операции типа int. - person MVittiS; 08.11.2016
comment
В auto accumulate_func = [](int accumulator, const Employee& emp) отсутствует открывающая фигурная скобка. - person Robur_131; 20.08.2019

Потому что стандартные библиотечные алгоритмы должны работать для произвольных диапазонов (совместимых) итераторов. Таким образом, первый аргумент accumulate не обязательно должен быть begin(), это может быть любой итератор от begin() до единицы перед end(). Также можно использовать обратные итераторы.

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

person juanchopanza    schedule 28.09.2012

Если бы вы хотели accumulate(V.begin()+1, V.end(), V.begin()), вы могли бы просто написать это. Но что, если вы думали, что v.begin () может быть v.end () (т.е. v пусто)? Что, если v.begin() + 1 не реализован (потому что v реализует только ++, а не обобщенное добавление)? Что делать, если тип аккумулятора не соответствует типу элементов? Например.

std::accumulate(v.begin(), v.end(), 0, [](long count, char c){
   return isalpha(c) ? count + 1 : count
});
person rici    schedule 28.09.2012
comment
v.begin() + 1 решена в C ++ 11 с введением std::next. С другой стороны, последний пункт (различие типов между разыменованным итератором и накопленным значением) является наиболее убедительным аргументом, который я видел до сих пор. - person Matthieu M.; 28.09.2012

Это действительно не нужно. Наша кодовая база имеет перегрузки с 2 и 3 аргументами, которые используют значение T{}.

Однако std::accumulate довольно старый; это происходит из оригинального STL. В нашей кодовой базе есть причудливая std::enable_if логика, позволяющая различать «2 итератора и начальное значение» и «2 итератора и оператор сокращения». Для этого требуется C ++ 11. В нашем коде также используется конечный возвращаемый тип (auto accumulate(...) -> ...) для вычисления возвращаемого типа, еще одна функция C ++ 11.

person MSalters    schedule 13.04.2018