¿Declarar variables dentro de bucles, buenas prácticas o malas prácticas?

266

Pregunta 1: ¿Declarar una variable dentro de un ciclo es una buena práctica o una mala práctica?

He leído los otros hilos sobre si existe o no un problema de rendimiento (la mayoría dijo que no), y que siempre debe declarar las variables tan cerca de donde se van a utilizar. Lo que me pregunto es si esto debería evitarse o no, o si realmente es preferible.

Ejemplo:

for(int counter = 0; counter <= 10; counter++)
{
   string someString = "testing";

   cout << someString;
}

Pregunta # 2: ¿La mayoría de los compiladores se dan cuenta de que la variable ya ha sido declarada y simplemente omiten esa parte, o cada vez crea un lugar para ella en la memoria?

JeramyRR
fuente
29
Póngalos cerca de su uso, a menos que el perfil indique lo contrario.
Mooing Duck
1
Aquí hay algunas preguntas similares: stackoverflow.com/questions/982963/… stackoverflow.com/questions/407255/…
drnewman
3
@drnewman Leí esos hilos, pero no respondieron mi pregunta. Entiendo que declarar variables dentro de bucles funciona. Me pregunto si es una buena práctica hacerlo o si es algo que debe evitarse.
JeramyRR

Respuestas:

348

Esta es una excelente práctica.

Al crear variables dentro de los bucles, se asegura de que su alcance esté restringido al interior del bucle. No se puede hacer referencia ni llamar fuera del bucle.

De esta manera:

  • Si el nombre de la variable es un poco "genérico" (como "i"), no hay riesgo de mezclarlo con otra variable del mismo nombre más adelante en su código (también puede mitigarse usando las -Wshadowinstrucciones de advertencia en GCC)

  • El compilador sabe que el alcance de la variable está limitado al interior del bucle y, por lo tanto, emitirá un mensaje de error adecuado si la variable se menciona por error en otro lugar.

  • Por último, pero no menos importante, el compilador puede realizar una optimización dedicada de manera más eficiente (lo más importante es la asignación de registros), ya que sabe que la variable no se puede usar fuera del ciclo. Por ejemplo, no es necesario almacenar el resultado para su reutilización posterior.

En resumen, tienes razón en hacerlo.

Sin embargo, tenga en cuenta que no se supone que la variable retenga su valor entre cada ciclo. En tal caso, es posible que deba inicializarlo cada vez. También puede crear un bloque más grande, que abarque el ciclo, cuyo único propósito es declarar variables que deben retener su valor de un ciclo a otro. Esto generalmente incluye el contador de bucle en sí.

{
    int i, retainValue;
    for (i=0; i<N; i++)
    {
       int tmpValue;
       /* tmpValue is uninitialized */
       /* retainValue still has its previous value from previous loop */

       /* Do some stuff here */
    }
    /* Here, retainValue is still valid; tmpValue no longer */
}

Para la pregunta # 2: la variable se asigna una vez, cuando se llama a la función. De hecho, desde una perspectiva de asignación, es (casi) lo mismo que declarar la variable al comienzo de la función. La única diferencia es el alcance: la variable no se puede usar fuera del bucle. Incluso puede ser posible que la variable no esté asignada, simplemente reutilizando algún espacio libre (de otra variable cuyo alcance ha finalizado).

Con un alcance restringido y más preciso vienen optimizaciones más precisas. Pero lo que es más importante, hace que su código sea más seguro, con menos estados (es decir, variables) de los que preocuparse al leer otras partes del código.

Esto es cierto incluso fuera de un if(){...}bloque. Por lo general, en lugar de:

    int result;
    (...)
    result = f1();
    if (result) then { (...) }
    (...)
    result = f2();
    if (result) then { (...) }

es más seguro escribir:

    (...)
    {
        int const result = f1();
        if (result) then { (...) }
    }
    (...)
    {
        int const result = f2();
        if (result) then { (...) }
    }

La diferencia puede parecer menor, especialmente en un ejemplo tan pequeño. Pero en una base de código más grande, ayudará: ahora no hay riesgo de transportar algún resultvalor desde f1()un f2()bloque. Cada uno resultestá estrictamente limitado a su propio alcance, lo que hace que su función sea más precisa. Desde la perspectiva del crítico, es mucho mejor, ya que tiene menos variables de estado de largo alcance de las que preocuparse y rastrear.

Incluso el compilador ayudará mejor: suponiendo que, en el futuro, después de algún cambio erróneo de código, resultno se inicialice correctamente f2(). La segunda versión simplemente se negará a funcionar, indicando un mensaje de error claro en tiempo de compilación (mucho mejor que el tiempo de ejecución). La primera versión no detectará nada, el resultado f1()simplemente se probará por segunda vez, confundiéndose con el resultado de f2().

Información complementaria

La herramienta de código abierto CppCheck (una herramienta de análisis estático para código C / C ++) proporciona algunos consejos excelentes sobre el alcance óptimo de las variables.

En respuesta al comentario sobre la asignación: la regla anterior es verdadera en C, pero podría no serlo para algunas clases de C ++.

Para los tipos y estructuras estándar, el tamaño de la variable se conoce en el momento de la compilación. No hay tal cosa como "construcción" en C, por lo que el espacio para la variable simplemente se asignará a la pila (sin ninguna inicialización), cuando se llama a la función. Es por eso que hay un costo "cero" al declarar la variable dentro de un bucle.

Sin embargo, para las clases de C ++, existe esta cosa de constructor de la que sé mucho menos. Supongo que la asignación probablemente no será el problema, ya que el compilador será lo suficientemente inteligente como para reutilizar el mismo espacio, pero es probable que la inicialización tenga lugar en cada iteración del bucle.

Cian
fuente
44
Impresionante respuesta. Esto es exactamente lo que estaba buscando, e incluso me dio una idea de algo que no sabía. No me di cuenta de que el alcance permanece solo dentro del bucle. ¡Gracias por la respuesta!
JeramyRR
22
"Pero nunca será más lento que asignar al comienzo de la función". Esto no siempre es cierto. La variable se asignará una vez, pero aún se construirá y destruirá tantas veces como sea necesario. Que en el caso del código de ejemplo, es 11 veces. Para citar el comentario de Mooing "Póngalos cerca de su uso, a menos que el perfil indique lo contrario".
IronMensan
44
@JeramyRR: Absolutamente no: el compilador no tiene forma de saber si el objeto tiene efectos secundarios significativos en su constructor o destructor.
ildjarn
2
@Iron: Por otro lado, cuando declaras el artículo primero, solo recibes muchas llamadas al operador de asignación; que generalmente cuesta casi lo mismo que construir y destruir un objeto.
Billy ONeal
44
@BillyONeal: para stringy vectorespecíficamente, el operador de asignación puede reutilizar el búfer asignado a cada ciclo, lo que (dependiendo de su ciclo) puede ser un gran ahorro de tiempo.
Mooing Duck el
22

En general, es una muy buena práctica mantenerlo muy cerca.

En algunos casos, habrá una consideración como el rendimiento que justifica sacar la variable del bucle.

En su ejemplo, el programa crea y destruye la cadena cada vez. Algunas bibliotecas utilizan una optimización de cadena pequeña (SSO), por lo que la asignación dinámica podría evitarse en algunos casos.

Supongamos que desea evitar esas creaciones / asignaciones redundantes, lo escribiría como:

for (int counter = 0; counter <= 10; counter++) {
   // compiler can pull this out
   const char testing[] = "testing";
   cout << testing;
}

o puedes sacar la constante:

const std::string testing = "testing";
for (int counter = 0; counter <= 10; counter++) {
   cout << testing;
}

¿La mayoría de los compiladores se dan cuenta de que la variable ya ha sido declarada y simplemente omiten esa parte, o en realidad crea un lugar para ella en la memoria cada vez?

Puede reutilizar el espacio que consume la variable y puede extraer invariantes de su ciclo. En el caso de la matriz const char (arriba), esa matriz podría extraerse. Sin embargo, el constructor y el destructor deben ejecutarse en cada iteración en el caso de un objeto (como std::string). En el caso de std::string, ese 'espacio' incluye un puntero que contiene la asignación dinámica que representa los caracteres. Así que esto:

for (int counter = 0; counter <= 10; counter++) {
   string testing = "testing";
   cout << testing;
}

requeriría una copia redundante en cada caso, y una asignación dinámica y libre si la variable se ubica por encima del umbral para el recuento de caracteres SSO (y su biblioteca estándar implementa SSO).

Haciendo esto:

string testing;
for (int counter = 0; counter <= 10; counter++) {
   testing = "testing";
   cout << testing;
}

aún requeriría una copia física de los caracteres en cada iteración, pero el formulario podría dar como resultado una asignación dinámica porque asigna la cadena y la implementación debería ver que no es necesario cambiar el tamaño de la asignación de respaldo de la cadena. Por supuesto, no haría eso en este ejemplo (porque ya se han demostrado múltiples alternativas superiores), pero podría considerarlo cuando varía el contenido de la cadena o del vector.

Entonces, ¿qué haces con todas esas opciones (y más)? Manténgalo muy cerca por defecto, hasta que comprenda bien los costos y sepa cuándo debe desviarse.

justin
fuente
1
Con respecto a los tipos de datos básicos como float o int, ¿declarar la variable dentro del bucle será más lento que declarar esa variable fuera del bucle ya que tendrá que asignar un espacio para la variable en cada iteración?
Kasparov92
2
@ Kasparov92 La respuesta corta es "No. Ignore esa optimización y colóquela en el ciclo cuando sea posible para mejorar la legibilidad / localidad. El compilador puede realizar esa microoptimización por usted". Más detalladamente, eso es en última instancia lo que debe decidir el compilador, en función de lo que sea mejor para la plataforma, los niveles de optimización, etc. Un int / float ordinario dentro de un bucle generalmente se colocará en la pila. Un compilador ciertamente puede mover eso fuera del ciclo y reutilizar el almacenamiento si hay una optimización para hacerlo. Para fines prácticos, esta sería una optimización muy muy muy pequeña ...
justin
1
@ Kasparov92 ... (cont) que solo consideraría en entornos / aplicaciones donde cada ciclo cuenta. En ese caso, es posible que desee considerar usar el ensamblaje.
Justin
14

Para C ++ depende de lo que esté haciendo. OK, es un código estúpido pero imagina

class myTimeEatingClass
{
 public:
 //constructor
      myTimeEatingClass()
      {
          sleep(2000);
          ms_usedTime+=2;
      }
      ~myTimeEatingClass()
      {
          sleep(3000);
          ms_usedTime+=3;
      }
      const unsigned int getTime() const
      {
          return  ms_usedTime;
      }
      static unsigned int ms_usedTime;
};
myTimeEatingClass::ms_CreationTime=0; 
myFunc()
{
    for (int counter = 0; counter <= 10; counter++) {

        myTimeEatingClass timeEater();
        //do something
    }
    cout << "Creating class took "<< timeEater.getTime() <<"seconds at all<<endl;

}
myOtherFunc()
{
    myTimeEatingClass timeEater();
    for (int counter = 0; counter <= 10; counter++) {
        //do something
    }
    cout << "Creating class took "<< timeEater.getTime() <<"seconds at all<<endl;

}

Esperará 55 segundos hasta que obtenga la salida de myFunc. Solo porque cada contructor de bucle y destructor juntos necesitan 5 segundos para terminar.

Necesitará 5 segundos hasta que obtenga la salida de myOtherFunc.

Por supuesto, este es un ejemplo loco.

Pero ilustra que podría convertirse en un problema de rendimiento cuando cada ciclo se realiza la misma construcción cuando el constructor y / o destructor necesita algo de tiempo.

Elegante
fuente
2
Bueno, técnicamente en la segunda versión obtendrá la salida en solo 2 segundos, porque aún no ha destruido el objeto .....
Chrys
12

No publiqué para responder las preguntas de JeremyRR (como ya han sido respondidas); en cambio, publiqué simplemente para dar una sugerencia.

Para JeremyRR, podrías hacer esto:

{
  string someString = "testing";   

  for(int counter = 0; counter <= 10; counter++)
  {
    cout << someString;
  }

  // The variable is in scope.
}

// The variable is no longer in scope.

No sé si se da cuenta (no lo hice cuando comencé a programar), que los corchetes (siempre que estén en pares) se pueden colocar en cualquier lugar dentro del código, no solo después de "if", "for", " mientras ", etc.

Mi código compilado en Microsoft Visual C ++ 2010 Express, así que sé que funciona; Además, intenté usar la variable fuera de los corchetes en los que se definió y recibí un error, por lo que sé que la variable fue "destruida".

No sé si es una mala práctica usar este método, ya que muchos paréntesis sin etiquetar podrían hacer que el código sea ilegible rápidamente, pero tal vez algunos comentarios podrían aclarar las cosas.

Fearnbuster
fuente
44
Para mí, esta es una respuesta muy legítima que trae una sugerencia directamente vinculada a la pregunta. Tienes mi voto!
Alexis Leclerc
0

Es una muy buena práctica, ya que todas las respuestas anteriores brindan un aspecto teórico muy bueno de la pregunta, déjenme echar un vistazo al código, estaba tratando de resolver DFS sobre GEEKSFORGEEKS, encuentro el problema de optimización ...... Si intenta resolver el código que declara el número entero fuera del ciclo le dará un error de optimización.

stack<int> st;
st.push(s);
cout<<s<<" ";
vis[s]=1;
int flag=0;
int top=0;
while(!st.empty()){
    top = st.top();
    for(int i=0;i<g[top].size();i++){
        if(vis[g[top][i]] != 1){
            st.push(g[top][i]);
            cout<<g[top][i]<<" ";
            vis[g[top][i]]=1;
            flag=1;
            break;
        }
    }
    if(!flag){
        st.pop();
    }
}

Ahora ponga números enteros dentro del ciclo, esto le dará la respuesta correcta ...

stack<int> st;
st.push(s);
cout<<s<<" ";
vis[s]=1;
// int flag=0;
// int top=0;
while(!st.empty()){
    int top = st.top();
    int flag = 0;
    for(int i=0;i<g[top].size();i++){
        if(vis[g[top][i]] != 1){
            st.push(g[top][i]);
            cout<<g[top][i]<<" ";
            vis[g[top][i]]=1;
            flag=1;
            break;
        }
    }
    if(!flag){
        st.pop();
    }
}

esto refleja completamente lo que dijo sir @justin en el segundo comentario ... intente esto aquí https://practice.geeksforgeeks.org/problems/depth-first-traversal-for-a-graph/1 . solo inténtalo ... lo conseguirás. Espero esta ayuda.

KhanJr
fuente
No creo que esto se aplique a la pregunta. Obviamente, en su caso anterior importa. La pregunta se refería al caso en el que la definición de la variable podría definirse en otro lugar sin cambiar el comportamiento del código.
pcarter
En el código que publicó, el problema no es la definición sino la parte de inicialización. flagdebe reinicializarse en 0 cada whileiteración. Ese es un problema de lógica, no un problema de definición.
Martin Véronneau
0

Capítulo 4.8 Estructura de bloques en el lenguaje de programación C de K&R 2.Ed. :

Una variable automática declarada e inicializada en un bloque se inicializa cada vez que se ingresa el bloque.

Podría haber extrañado ver la descripción relevante en el libro como:

Una variable automática declarada e inicializada en un bloque se asigna solo una vez antes de ingresar el bloque.

Pero una prueba simple puede probar la suposición sostenida:

 #include <stdio.h>                                                                                                    

 int main(int argc, char *argv[]) {                                                                                    
     for (int i = 0; i < 2; i++) {                                                                                     
         for (int j = 0; j < 2; j++) {                                                                                 
             int k;                                                                                                    
             printf("%p\n", &k);                                                                                       
         }                                                                                                             
     }                                                                                                                 
     return 0;                                                                                                         
 }                                                                                                                     
sof
fuente