c++ - librerias - ¿Debería uno preferir los algoritmos STL sobre los lazos enrollados a mano?
vector stl c++ ejemplos (11)
Creo que la interfaz del algoritmo STL no es óptima y debe evitarse, ya que usar el kit de herramientas STL directamente (para algoritmos) podría ofrecer una ganancia muy pequeña en el rendimiento, pero definitivamente le costará legibilidad, capacidad de mantenimiento e incluso un poco de capacidad de escritura cuando " volver a aprender a usar las herramientas.
Cuanto más eficiente es un estándar para el bucle sobre un vector:
int weighted_sum = 0;
for (int i = 0; i < a_vector.size(); ++i) {
weighted_sum += (i + 1) * a_vector[i]; // Just writing something a little nontrivial.
}
que usar una construcción para cada curso, o tratar de encajar esto en una llamada para acumular?
Podría argumentar que el proceso de iteración es menos eficiente, pero para cada uno también introduce una llamada de función en cada paso (que podría mitigarse intentando alinear la función, pero recuerde que "en línea" es solo una sugerencia para el compilador). puede ignorarlo).
En cualquier caso, la diferencia es pequeña. Según mi experiencia, más del 90% del código que se escribe no es crítico para el rendimiento, pero es crucial para el tiempo del codificador . Al mantener su bucle STL literalmente en línea, es muy legible. Hay menos indirección para tropezar, para usted o los futuros mantenedores. Si está en su guía de estilo, entonces está ahorrando algo de tiempo de aprendizaje para sus codificadores (admítalo, aprender a usar correctamente el STL la primera vez implica unos pocos momentos de gotcha). Esto último es lo que quiero decir con un costo en escriturabilidad.
Por supuesto, hay algunos casos especiales, por ejemplo, es posible que desee que cada función se separe para volver a utilizarla en otros lugares. O bien, podría ser una de esas pocas secciones de alto rendimiento crítico. Pero estos son casos especiales: excepciones en lugar de la regla.
Parece que estoy viendo más ''por'' bucles sobre iteradores en preguntas y respuestas aquí que en for_each (), transform () y similares. Scott Meyers sugiere que se prefieren los algoritmos Stl , o al menos lo hizo en 2001. Por supuesto, su uso a menudo significa mover el cuerpo del bucle en una función o objeto de función. Algunos pueden sentir que se trata de una complicación inaceptable, mientras que otros pueden sentir que es mejor solucionar el problema.
Entonces ... ¿deberían preferirse los algoritmos STL a los bucles lanzados a mano?
Creo que un factor importante es el nivel de comodidad del desarrollador.
Probablemente sea cierto que usar transform o for_each es lo correcto, pero no es más eficiente, y los bucles manuscritos no son intrínsecamente peligrosos. Si a un desarrollador le llevaría media hora escribir un bucle simple, frente a medio día para obtener la sintaxis de transformación o de cada derecha, y mover el código proporcionado a una función u objeto de función. Y luego otros desarrolladores necesitarían saber qué estaba pasando.
Un nuevo desarrollador probablemente sería mejor si aprendiera a usar transformaciones y para cada uno en lugar de bucles hechos a mano, ya que podría usarlos consistentemente sin errores. Para el resto de nosotros, para quienes escribir bucles es algo natural, probablemente sea mejor seguir con lo que sabemos y familiarizarnos con los algoritmos en nuestro tiempo libre.
Ponlo de esta manera: si le digo a mi jefe que me he pasado el día convirtiendo los bucles hechos a mano en for_each y transformando las llamadas, dudo que esté muy contento.
Depende, si el algoritmo no toma un funtor, entonces siempre use la versión del algoritmo std. Es más simple para ti escribir y más claro.
Para los algoritmos que toman functors, generalmente no, hasta que se pueda usar C ++ 0x lambdas. Si el funtor es pequeño y el algoritmo es complejo (la mayoría no lo son), entonces puede ser mejor usar el algoritmo std.
El bucle for
es imperativo, los algoritmos son declarativos. Cuando escribe std::max_element
, es obvio lo que necesita, cuando usa un bucle para lograr lo mismo, no es necesariamente así.
Los algoritmos también pueden tener un ligero margen de rendimiento. Por ejemplo, al atravesar un std::deque
, un algoritmo especializado puede evitar la verificación redundante de si un incremento determinado mueve el puntero sobre un límite de fragmento.
Sin embargo, las expresiones de functor complicadas rápidamente hacen que las invocaciones de algoritmo sean ilegibles. Si un ciclo explícito es más legible, úselo. Si una llamada de algoritmo puede expresarse sin expresiones de enlace de diez plantas, por supuesto que la prefiere. La legibilidad es más importante que el rendimiento aquí , porque este tipo de optimización es lo que Knuth tan famoso le atribuye a Hoare; podrás utilizar otro constructo sin problemas una vez que te des cuenta de que es un cuello de botella.
Esa es realmente la única cosa que Scott Meyers se equivocó.
Si hay un algoritmo real que coincida con lo que debe hacer, entonces, por supuesto, use el algoritmo.
Pero si todo lo que necesita hacer es recorrer una colección y hacer algo para cada elemento, simplemente haga el ciclo normal en lugar de tratar de separar el código en un diverso funtor, que acaba dividiendo el código en bits sin ninguna ganancia real.
Hay algunas otras opciones como boost :: bind o boost :: lambda, pero esas son tareas de metaprogramación de plantillas realmente complejas, no funcionan muy bien con la depuración y el paso por el código, por lo que generalmente deben evitarse.
Como han mencionado otros, todo esto cambiará cuando las expresiones lambda se conviertan en ciudadanos de primera clase.
No usaría una regla dura y rápida para eso. Hay muchos factores a considerar, como que a menudo se realiza cierta operación en el código, es solo un ciclo o un algoritmo "real", ¿el algoritmo depende de un montón de contexto que tendrías que transmitir a tu función?
Por ejemplo, no pondría algo así como
for (int i = 0; i < some_vector.size(); i++)
if (some_vector[i] == NULL) some_other_vector[i]++;
en un algoritmo porque daría como resultado un porcentaje de código mucho mayor y tendría que lidiar con que el algoritmo conociera some_other_vector de alguna manera.
Hay muchos otros ejemplos en los que el uso de algoritmos STL tiene mucho sentido, pero debe decidir caso por caso.
El std::foreach
es el tipo de código que me hizo maldecir el STL, hace años.
No puedo decir si es mejor, pero me gusta más tener el código de mi ciclo bajo el preámbulo del ciclo. Para mí, es un requisito fuerte . Y la construcción std::foreach
no me permitirá eso (por extraño que parezca, las versiones foreach de Java o C # son geniales, en lo que a mí respecta ... Así que supongo que confirma que para mí la localidad del cuerpo del bucle es muy muy importante).
Así que usaré el foreach solo si solo hay un algoritmo legible / comprensible que se pueda usar con él. Si no, no, no lo haré. Pero esto es una cuestión de gusto, supongo, ya que quizás debería esforzarme más por comprender y aprender a analizar todo esto ...
Tenga en cuenta que las personas en boost aparentemente se sentían de la misma manera, ya que escribieron BOOST_FOREACH:
#include <string>
#include <iostream>
#include <boost/foreach.hpp>
int main()
{
std::string hello( "Hello, world!" );
BOOST_FOREACH( char ch, hello )
{
std::cout << ch;
}
return 0;
}
Ver: http://www.boost.org/doc/libs/1_35_0/doc/html/foreach.html
Soy un gran admirador de los algoritmos STL en principio, pero en la práctica es demasiado engorroso. En el momento en que define sus clases de functor / predicado, un bucle de dos líneas puede convertirse en más de 40 líneas de código que de repente es 10 veces más difícil de descifrar.
Afortunadamente, las cosas van a ser mucho más fáciles en C ++ 0x con funciones lambda, auto
y nuevo for
sintaxis. Consulte esta descripción general de C ++ 0x en Wikipedia.
OMI, se deben evitar muchos algoritmos estándar de biblioteca como std :: for_each, principalmente por los problemas de falta de lambda mencionados por otros, pero también porque existe una ocultación de detalles inapropiada.
Por supuesto, ocultar detalles en funciones y clases es parte de la abstracción y, en general, una abstracción de la biblioteca es mejor que reinventar la rueda. Pero una habilidad clave con la abstracción es saber cuándo hacerlo y cuándo no hacerlo. La abstracción excesiva puede dañar la legibilidad, la capacidad de mantenimiento, etc. El buen juicio viene con la experiencia, no con reglas inflexibles, aunque debes aprender las reglas antes de aprender a romperlas, por supuesto.
OTOH, vale la pena considerar el hecho de que muchos programadores han estado usando C ++ (y antes de eso, C, Pascal, etc.) durante mucho tiempo. Los viejos hábitos se mueren duro, y existe esta cosa llamada disonancia cognitiva que a menudo conduce a excusas y racionalizaciones. Sin embargo, no saque conclusiones: es al menos tan probable que los tipos sean culpables de disonancia posterior a la toma de decisiones.
Voy a ir contra la corriente aquí y defiendo que el uso de algoritmos STL con funtores hace que el código sea mucho más fácil de entender y mantener, pero tienes que hacerlo bien. Debes prestar más atención a la legibilidad y claridad. Particularmente, debes obtener el nombre correcto. Pero cuando lo haga, puede terminar con un código más claro y claro y un cambio de paradigma en técnicas de codificación más potentes.
Tomemos un ejemplo. Aquí tenemos un grupo de niños, y queremos establecer su "Conteo Foo" a algún valor. El enfoque iterativo estándar para bucle es:
for (vector<Child>::iterator iter = children.begin();
iter != children.end();
++iter)
{
iter->setFooCount(n);
}
Lo cual, sí, está bastante claro, y definitivamente no es un código malo . Puedes resolverlo solo con un poco de mirarlo. Pero mira lo que podemos hacer con un functor apropiado:
for_each(children.begin(), children.end(), SetFooCount(n));
Wow, eso dice exactamente lo que necesitamos. No tienes que resolverlo; inmediatamente sabe que está configurando el "Conteo Foo" de cada niño. (Sería aún más claro si no necesitáramos las tonterías de .begin () / .end (), pero no puede tener todo, y no me consultaron al hacer el STL).
Por supuesto, necesitas definir este functor mágico, SetFooCount
, pero su definición es bastante repetitiva:
class SetFooCount
{
public:
SetFooCount(int n) : fooCount(n) {}
void operator () (Child& child)
{
child.setFooCount(fooCount);
}
private:
int fooCount;
};
En total es más código, y tienes que mirar en otro lugar para averiguar exactamente qué está haciendo SetFooCount
. Pero debido a que lo SetFooCount
bien, el 99% de las veces no tenemos que mirar el código de SetFooCount
. Suponemos que hace lo que dice, y solo tenemos que mirar la línea for_each
.
Lo que realmente me gusta es que usar los algoritmos conduce a un cambio de paradigma. En lugar de pensar en una lista como una colección de objetos y hacer cosas para cada elemento de la lista, piensas en la lista como una entidad de primera clase, y operas directamente en la propia lista. For-loop itera a través de la lista, llamando a una función miembro en cada elemento para configurar el Foo Count. En cambio, estoy haciendo un comando, que establece el recuento Foo de cada elemento de la lista. Es sutil, pero cuando miras el bosque en lugar de los árboles, obtienes más poder.
Entonces, con un poco de reflexión y una nomenclatura cuidadosa, podemos usar los algoritmos STL para crear un código más claro y claro, y comenzar a pensar en un nivel menos granular.
Depende de:
- Si se requiere un alto rendimiento
- La legibilidad del ciclo
- Si el algoritmo es complejo
Si el ciclo no es el cuello de botella, y el algoritmo es simple (como para cada uno), entonces para el estándar actual de C ++, preferiría un lazo enrollado a mano para la legibilidad. (La localización de la lógica es la clave)
Sin embargo, ahora que C ++ 0x / C ++ 11 es compatible con algunos compiladores importantes, yo diría que usan algoritmos STL porque ahora permiten expresiones lambda, y por lo tanto, la localidad de la lógica.