¿Por qué usar funtores sobre funciones?

Comparar

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

con

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

¿Cuáles son los beneficios de usar un functor sobre una función? ¿No es el primero mucho más fácil de leer (incluso antes de que se agregue la implementación)?

Supongamos que el funtor se define así:

 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; } }; 

Al menos cuatro buenas razones:

Separación de intereses

En su ejemplo particular, el enfoque basado en el functor tiene la ventaja de separar la lógica de iteración de la lógica de cálculo promedio. Entonces puede usar su functor en otras situaciones (piense en todos los otros algoritmos en el STL), y puede usar otros funtores con for_each .

Parametrización

Puede parametrizar un functor más fácilmente. Así, por ejemplo, podría tener un functor CalculateAverageOfPowers que tome el promedio de los cuadrados, cubos, etc. de sus datos, que se escribirían así:

 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; }; 

Por supuesto, puede hacer lo mismo con una función tradicional, pero luego lo hace difícil de usar con punteros a función, ya que tiene un prototipo diferente para CalculateAverage .

Estado de alerta

Y como los funtores pueden ser concisos, podrías hacer algo como esto:

 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); 

promediar en varios conjuntos de datos diferentes.

Tenga en cuenta que casi todos los algoritmos / contenedores STL que aceptan funtores requieren que sean predicados “puros”, es decir, no tengan ningún cambio observable en el estado a lo largo del tiempo. for_each es un caso especial a este respecto (véase, por ejemplo, la Biblioteca estándar efectiva de C ++ – for_each vs. transform ).

Actuación

Los funtores a menudo pueden ser insertados por el comstackdor (el STL es un conjunto de plantillas, después de todo). Si bien lo mismo es teóricamente cierto para las funciones, los comstackdores generalmente no se alinearán a través de un puntero a la función. El ejemplo canónico es comparar std::sort vs qsort ; la versión de STL suele ser 5-10 veces más rápida, suponiendo que el predicado de comparación en sí es simple.

Resumen

Por supuesto, es posible emular los primeros tres con funciones y punteros tradicionales, pero se vuelve mucho más simple con los funtores.

Ventajas de los Functors:

  • A diferencia de las funciones Functor puede tener estado.
  • Functor se ajusta al paradigma de OOP en comparación con las funciones.
  • El Functor a menudo puede estar alineado a diferencia de los punteros de función
  • Functor no requiere el envío de vtable y runtime, y por lo tanto es más eficiente en la mayoría de los casos.

std::for_each es fácilmente el algoritmo estándar más caprichoso y menos útil. Es solo una buena envoltura para un bucle. Sin embargo, incluso tiene ventajas.

Considere cómo debe ser su primera versión de CalculateAverage . Tendrá un ciclo sobre los iteradores, y luego hará cosas con cada elemento. ¿Qué pasa si escribes ese bucle incorrectamente? Oops; hay un comstackdor o error de tiempo de ejecución. La segunda versión nunca puede tener tales errores. Sí, no es mucho código, pero ¿por qué tenemos que escribir bucles con tanta frecuencia? ¿Por qué no solo una vez?

Ahora, considere algoritmos reales ; los que realmente funcionan ¿Quieres escribir std::sort ? O std::find ? O std::nth_element ? ¿Sabes cómo implementarlo de la manera más eficiente posible? ¿Cuántas veces desea implementar estos complejos algoritmos?

En cuanto a la facilidad de lectura, eso está en los ojos del espectador. Como dije, std::for_each no es la primera opción para los algoritmos (especialmente con la syntax basada en el rango de C ++ 0x). Pero si se trata de algoritmos reales, son muy legibles; std::sort ordena una lista. Algunos de los más oscuros como std::nth_element no serán tan familiares, pero siempre puedes buscarlos en tu útil referencia de C ++.

E incluso std :: for_each es perfectamente legible una vez que utiliza Lambda en C ++ 0x.

En el primer enfoque, el código de iteración debe duplicarse en todas las funciones que quieran hacer algo con la colección. El segundo enfoque oculta los detalles de la iteración.

• A diferencia de Functions Functor puede tener estado.

Esto es muy interesante porque std :: binary_function, std :: less y std :: equal_to tiene una plantilla para un operador () que es const. Pero, ¿y si quisieras imprimir un mensaje de depuración con el conteo de llamadas actual para ese objeto, cómo lo harías?

Aquí está la plantilla para std :: equal_to:

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

Puedo pensar en 3 formas de permitir que el operador () sea const y, sin embargo, cambie una variable miembro. Pero, ¿cuál es la mejor manera? Toma este ejemplo:

 #include  #include  #include  #include  #include  // 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 { 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 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(&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(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"; } 

Debido a que las 3 soluciones están comstackdas, incrementa el recuento en 3. Aquí está el resultado:

 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 

OOP es la palabra clave aquí.

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

4.1 ¿Qué son los Funtores?

Los funtores son funciones con un estado. En C ++ puede realizarlos como una clase con uno o más miembros privados para almacenar el estado y con un operador sobrecargado () para ejecutar la función. Los funtores pueden encapsular punteros de función C y C ++ empleando las plantillas de conceptos y el polymorphism. Puede crear una lista de punteros a funciones miembro de clases arbitrarias y llamarlas a todas a través de la misma interfaz sin preocuparse por su clase o la necesidad de un puntero a una instancia. Todas las funciones solo tienen que tener los mismos parámetros de devolución y llamada. A veces los funtores también se conocen como cierres. También puede usar funtores para implementar devoluciones de llamada.

Está comparando funciones en diferentes niveles de abstracción.

Puede implementar CalculateAverage(begin, end) como:

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

o puedes hacerlo con un bucle for

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

El primero requiere que sepas más cosas, pero una vez que las conoces, es más simple y deja menos posibilidades de error.

También utiliza solo dos componentes generics ( std::accumulate y std::plus ), que a menudo también es el caso más complejo. A menudo puede tener un functor (o función) simple y universal, la función antigua simple puede actuar como un funtor) y simplemente combinarlo con cualquier algoritmo que necesite.