Зачем использовать функторы над функциями? - PullRequest
54 голосов
/ 23 июня 2011

Сравните

double average = CalculateAverage(values.begin(), values.end());

с

double average = std::for_each(values.begin(), values.end(), CalculateAverage());

Каковы преимущества использования функтора над функцией?Разве первое не легче прочитать (даже до добавления реализации)?

Предположим, что функтор определен так:

class CalculateAverage
{
private:
   std::size_t num;
   double sum;
public:

   CalculateAverage() : num (0) , sum (0)
   {
   }

   void operator () (double elem) 
   {
      num++; 
      sum += elem;
   }

   operator double() const
   {
       return sum / num;
   }
};

Ответы [ 7 ]

74 голосов
/ 23 июня 2011

Как минимум четыре веские причины:

Разделение задач

В вашем конкретном примере основанный на функторе подход имеет преимущество отделения логики итерации отлогика усреднения.Таким образом, вы можете использовать ваш функтор в других ситуациях (подумайте обо всех других алгоритмах в STL), и вы можете использовать другие функторы с for_each.

Параметризация

Вы можете более легко параметризировать функтор.Так, например, у вас может быть функтор CalculateAverageOfPowers, который принимает среднее значение квадратов или кубов и т. Д. Ваших данных, которые будут записаны следующим образом:

class CalculateAverageOfPowers
{
public:
    CalculateAverageOfPowers(float p) : acc(0), n(0), p(p) {}
    void operator() (float x) { acc += pow(x, p); n++; }
    float getAverage() const { return acc / n; }
private:
    float acc;
    int   n;
    float p;
};

Конечно, вы можете выполнитьТо же самое с традиционной функцией, но это затрудняет использование с указателями на функции, потому что она имеет прототип, отличный от CalculateAverage.

Statefulness

И какфункторы могут быть с состоянием, вы можете сделать что-то вроде этого:

CalculateAverage avg;
avg = std::for_each(dataA.begin(), dataA.end(), avg);
avg = std::for_each(dataB.begin(), dataB.end(), avg);
avg = std::for_each(dataC.begin(), dataC.end(), avg);

для усреднения по множеству различных наборов данных.

Обратите внимание, что почти все алгоритмы / контейнеры STL, которые принимают функторы, требуютони должны быть «чистыми» предикатами, то есть не иметь видимых изменений в состоянии с течением времени.for_each является особым случаем в этом отношении (см., Например, Эффективная стандартная библиотека C ++ - for_each против преобразования ).

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

Функторычасто может быть встроен компилятором (в конце концов, STL - это набор шаблонов).Хотя то же самое теоретически верно для функций, компиляторы обычно не встроены через указатель на функцию.Канонический пример - сравнение std::sort против qsort;версия STL часто в 5-10 раз быстрее, при условии, что сам предикат сравнения прост.

Сводка

Конечно, можно эмулировать первые три с традиционными функциямии указатели, но с функторами становится намного проще.

9 голосов
/ 23 июня 2011

Преимущества Функторов:

  • В отличие от функций Functor может иметь состояние.
  • Функтор вписывается в парадигму ООП по сравнению с функциями.
  • Функтор часто может быть встроенным в отличие от указателей на функции
  • Functor не требует vtable и диспетчерской диспетчеризации, и, следовательно, более эффективен в большинстве случаев.
7 голосов
/ 23 июня 2011

std::for_each - самый капризный и наименее полезный из стандартных алгоритмов. Это просто хорошая обертка для петли. Однако даже у него есть свои преимущества.

Подумайте, как должна выглядеть ваша первая версия CalculateAverage. Он будет иметь цикл над итераторами, а затем делать вещи с каждым элементом. Что произойдет, если вы напишите этот цикл неправильно? К сожалению, есть ошибка компилятора или времени выполнения. Вторая версия никогда не может иметь таких ошибок. Да, кода немного, но почему мы так часто пишем циклы? Почему не один раз?

Теперь рассмотрим алгоритмы real ; те, которые действительно работают. Вы хотите написать std::sort? Или std::find? Или std::nth_element? Вы даже знаете, как реализовать это наиболее эффективным способом? Сколько раз вы хотите реализовать эти сложные алгоритмы?

Что касается легкости чтения, это в глазах смотрящего. Как я уже сказал, std::for_each вряд ли является первым выбором для алгоритмов (особенно с C ++ 0x на основе диапазона для синтаксиса). Но если вы говорите о реальных алгоритмах, они очень удобочитаемы; std::sort сортирует список. Некоторые из более неясных, таких как std::nth_element, не будут такими знакомыми, но вы всегда можете посмотреть их в удобном справочнике по C ++.

И даже std :: for_each отлично читается, когда вы используете лямбда-выражения в C ++ 0x.

2 голосов
/ 14 февраля 2013

• В отличие от функций Functor может иметь состояние.

Это очень интересно, потому что std :: binary_function, std :: less и std :: equal_to имеют шаблон для оператора (), который является const. Но что, если вы хотите напечатать отладочное сообщение с текущим счетчиком вызовов для этого объекта, как бы вы это сделали?

Вот шаблон для std :: equal_to:

struct equal_to : public binary_function<_Tp, _Tp, bool>
{
  bool
  operator()(const _Tp& __x, const _Tp& __y) const
  { return __x == __y; }
};

Я могу придумать 3 способа, позволяющих оператору () быть константным, и при этом изменить переменную-член. Но как лучше? Возьмите этот пример:

#include <iostream>
#include <string>
#include <algorithm>
#include <functional>
#include <cassert>  // assert() MACRO

// functor for comparing two integer's, the quotient when integer division by 10.
// So 50..59 are same, and 60..69 are same.
// Used by std::sort()

struct lessThanByTen: public std::less<int>
{
private:
    // data members
    int count;  // nr of times operator() was called

public:
    // default CTOR sets count to 0
    lessThanByTen() :
        count(0)
    {
    }


    // @override the bool operator() in std::less<int> which simply compares two integers
    bool operator() ( const int& arg1, const int& arg2) const
    {
        // this won't compile, because a const method cannot change a member variable (count)
//      ++count;


        // Solution 1. this trick allows the const method to change a member variable
        ++(*(int*)&count);

        // Solution 2. this trick also fools the compilers, but is a lot uglier to decipher
        ++(*(const_cast<int*>(&count)));

        // Solution 3. a third way to do same thing:
        {
        // first, stack copy gets bumped count member variable
        int incCount = count+1;

        const int *iptr = &count;

        // this is now the same as ++count
        *(const_cast<int*>(iptr)) = incCount;
        }

        std::cout << "DEBUG: operator() called " << count << " times.\n";

        return (arg1/10) < (arg2/10);
    }
};

void test1();
void printArray( const std::string msg, const int nums[], const size_t ASIZE);

int main()
{
    test1();
    return 0;
}

void test1()
{
    // unsorted numbers
    int inums[] = {33, 20, 10, 21, 30, 31, 32, 22, };

    printArray( "BEFORE SORT", inums, 8 );

    // sort by quotient of integer division by 10
    std::sort( inums, inums+8, lessThanByTen() );

    printArray( "AFTER  SORT", inums, 8 );

}

//! @param msg can be "this is a const string" or a std::string because of implicit string(const char *) conversion.
//! print "msg: 1,2,3,...N", where 1..8 are numbers in nums[] array

void printArray( const std::string msg, const int nums[], const size_t ASIZE)
{
    std::cout << msg << ": ";
    for (size_t inx = 0; inx < ASIZE; ++inx)
    {
        if (inx > 0)
            std::cout << ",";
        std::cout << nums[inx];
    }
    std::cout << "\n";
}

Поскольку все 3 решения компилируются, счетчик увеличивается на 3. Вот результат:

gcc -g -c Main9.cpp
gcc -g Main9.o -o Main9 -lstdc++
./Main9
BEFORE SORT: 33,20,10,21,30,31,32,22
DEBUG: operator() called 3 times.
DEBUG: operator() called 6 times.
DEBUG: operator() called 9 times.
DEBUG: operator() called 12 times.
DEBUG: operator() called 15 times.
DEBUG: operator() called 12 times.
DEBUG: operator() called 15 times.
DEBUG: operator() called 15 times.
DEBUG: operator() called 18 times.
DEBUG: operator() called 18 times.
DEBUG: operator() called 21 times.
DEBUG: operator() called 21 times.
DEBUG: operator() called 24 times.
DEBUG: operator() called 27 times.
DEBUG: operator() called 30 times.
DEBUG: operator() called 33 times.
DEBUG: operator() called 36 times.
AFTER  SORT: 10,20,21,22,33,30,31,32
2 голосов
/ 23 июня 2011

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

1 голос
/ 23 июня 2011

Вы сравниваете функции на разных уровнях абстракции.

Вы можете реализовать CalculateAverage(begin, end) как:

template<typename Iter>
double CalculateAverage(Iter begin, Iter end)
{
    return std::accumulate(begin, end, 0.0, std::plus<double>) / std::distance(begin, end)
}

или вы можете сделать это с помощью цикла for

template<typename Iter>
double CalculateAverage(Iter begin, Iter end)
{
    double sum = 0;
    int count = 0;
    for(; begin != end; ++begin) {
        sum += *begin;
        ++count;
    }
    return sum / count;
}

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

Он также использует только два общих компонента (std::accumulate и std::plus), что часто бывает и в более сложных случаях.Часто вы можете иметь простой универсальный функтор (или функцию; простая старая функция может выступать в качестве функтора) и просто комбинировать ее с любым алгоритмом, который вам нужен.

1 голос

ООП является ключевым словом здесь.

http://www.newty.de/fpt/functor.html:

4.1 Что такое функторы?

Функторы - это функции с состоянием. В C ++ вы можете реализовать их как класс с одним или несколькими закрытыми членами для хранения состояния и с перегруженным оператором () для выполнения функции. Функторы могут инкапсулировать указатели на функции C и C ++, используя шаблоны концепций и полиморфизм. Вы можете создать список указателей на функции-члены произвольных классов и вызывать их через один и тот же интерфейс, не заботясь об их классе или необходимости указателя на экземпляр. Все функции должны иметь одинаковый тип возврата и параметры вызова. Иногда функторы также известны как замыкания. Вы также можете использовать функторы для реализации обратных вызовов.

...