Dichiarare variabili all'interno di loop, buone pratiche o cattive pratiche?


266

Domanda n. 1: dichiarare una variabile all'interno di un ciclo è una buona pratica o una cattiva pratica?

Ho letto gli altri thread sull'esistenza o meno di un problema di prestazioni (la maggior parte ha detto di no) e che dovresti sempre dichiarare le variabili vicine al punto in cui verranno utilizzate. Quello che mi chiedo è se questo dovrebbe essere evitato o meno se è effettivamente preferito.

Esempio:

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

   cout << someString;
}

Domanda n. 2: la maggior parte dei compilatori si rende conto che la variabile è già stata dichiarata e salta semplicemente quella porzione o crea effettivamente un punto per essa ogni volta in memoria?


29
Metterli vicino al loro utilizzo, a meno che la profilazione non dica diversamente.
Mooing Duck,


3
@drnewman Ho letto quei thread, ma non hanno risposto alla mia domanda. Comprendo che la dichiarazione delle variabili all'interno dei loop funziona. Mi chiedo se sia una buona pratica farlo o se è qualcosa da evitare.
Jeramy,

Risposte:


348

Questa è una pratica eccellente .

Creando variabili all'interno dei loop, si garantisce che il loro ambito sia limitato all'interno del loop. Non può essere referenziato né chiamato al di fuori del ciclo.

Per di qua:

  • Se il nome della variabile è un po '"generico" (come "i"), non vi è alcun rischio di mescolarlo con un'altra variabile con lo stesso nome da qualche parte più avanti nel codice (può anche essere mitigato usando le -Wshadowistruzioni di avviso su GCC)

  • Il compilatore sa che l'ambito della variabile è limitato all'interno del ciclo e quindi emetterà un messaggio di errore corretto se la variabile viene referenziata per errore altrove.

  • Ultimo ma non meno importante, alcune funzioni di ottimizzazione dedicate possono essere eseguite in modo più efficiente dal compilatore (soprattutto l'allocazione dei registri), poiché sa che la variabile non può essere utilizzata al di fuori del ciclo. Ad esempio, non è necessario archiviare il risultato per un riutilizzo successivo.

In breve, hai ragione a farlo.

Si noti tuttavia che la variabile non deve mantenere il suo valore tra ciascun ciclo. In tal caso, potrebbe essere necessario inizializzarlo ogni volta. Puoi anche creare un blocco più grande, comprendente il ciclo, il cui unico scopo è dichiarare variabili che devono conservare il loro valore da un ciclo all'altro. Ciò include in genere il contatore di loop stesso.

{
    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 */
}

Per la domanda n. 2: la variabile viene allocata una volta, quando viene chiamata la funzione. In effetti, dal punto di vista dell'allocazione, è (quasi) lo stesso che dichiarare la variabile all'inizio della funzione. L'unica differenza è l'ambito: la variabile non può essere utilizzata al di fuori del ciclo. Potrebbe anche essere possibile che la variabile non sia allocata, semplicemente riutilizzando alcuni slot liberi (da altre variabili il cui ambito è terminato).

Con un ambito limitato e più preciso arrivano ottimizzazioni più accurate. Ma soprattutto, rende il tuo codice più sicuro, con meno stati (cioè variabili) di cui preoccuparti quando leggi altre parti del codice.

Questo è vero anche al di fuori di un if(){...}blocco. In genere, invece di:

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

è più sicuro scrivere:

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

La differenza può sembrare minore, specialmente su un esempio così piccolo. Ma su una base di codice più grande, vi aiuterà: ora non v'è alcun rischio per il trasporto di un certo resultvalore da f1()al f2()blocco. Ognuno resultè strettamente limitato al proprio scopo, rendendo il suo ruolo più preciso. Dal punto di vista del recensore, è molto più bello, dal momento che ha variabili di stato a lungo raggio di cui preoccuparsi e tenere traccia.

Anche il compilatore aiuterà meglio: supponendo che, in futuro, dopo qualche errata modifica del codice, resultnon verrà correttamente inizializzato con f2(). La seconda versione si rifiuterà semplicemente di funzionare, dichiarando un chiaro messaggio di errore in fase di compilazione (molto meglio del tempo di esecuzione). La prima versione non individuerà nulla, il risultato f1()sarà semplicemente testato una seconda volta, confuso per il risultato di f2().

Informazioni complementari

Lo strumento open source CppCheck (uno strumento di analisi statica per il codice C / C ++) fornisce alcuni suggerimenti eccellenti riguardo l'ambito ottimale delle variabili.

In risposta al commento sull'allocazione: la regola sopra è vera in C, ma potrebbe non essere valida per alcune classi C ++.

Per tipi e strutture standard, la dimensione della variabile è nota al momento della compilazione. Non esiste una cosa come "costruzione" in C, quindi lo spazio per la variabile verrà semplicemente allocato nello stack (senza alcuna inizializzazione), quando viene chiamata la funzione. Ecco perché c'è un costo "zero" quando si dichiara la variabile all'interno di un ciclo.

Tuttavia, per le classi C ++, c'è questa cosa del costruttore che conosco molto meno. Immagino che l'allocazione non sarà probabilmente il problema, dal momento che il compilatore sarà abbastanza intelligente da riutilizzare lo stesso spazio, ma è probabile che l'inizializzazione avvenga ad ogni iterazione di loop.


4
Risposta eccezionale. Questo è esattamente quello che stavo cercando e mi ha persino dato un'idea di qualcosa che non avevo realizzato. Non mi ero reso conto che l'ambito rimanesse solo all'interno del loop. Grazie per la risposta!
JeramyRR

22
"Ma non sarà mai più lento dell'allocazione all'inizio della funzione." Questo non è sempre vero. La variabile verrà allocata una volta, ma sarà comunque costruita e distrutta tutte le volte che è necessario. Che nel caso del codice di esempio, è 11 volte. Per citare il commento di Mooing "Mettili vicini al loro utilizzo, a meno che la profilazione non dica diversamente".
IronMensan,

4
@JeramyRR: Assolutamente no - il compilatore non ha modo di sapere se l'oggetto ha effetti collaterali significativi nel suo costruttore o distruttore.
ildjarn,

2
@Iron: D'altra parte, quando dichiari per primo l'articolo, ricevi solo molte chiamate all'operatore di assegnazione; che in genere ha lo stesso costo di costruzione e distruzione di un oggetto.
Billy ONeal

4
@BillyONeal: per stringe in vectorparticolare, l'operatore di assegnazione può riutilizzare il buffer allocato in ciascun loop, il che (a seconda del loop) può comportare un notevole risparmio di tempo.
Mooing Duck

22

In generale, è una buona pratica tenerlo molto vicino.

In alcuni casi, ci sarà una considerazione come performance che giustifica l'estrazione della variabile dal loop.

Nel tuo esempio, il programma crea e distrugge la stringa ogni volta. Alcune librerie utilizzano una piccola stringa di ottimizzazione (SSO), pertanto in alcuni casi è possibile evitare l'allocazione dinamica.

Supponiamo che tu voglia evitare quelle creazioni / allocazioni ridondanti, lo scriveresti come:

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

oppure puoi estrarre la costante:

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

La maggior parte dei compilatori si rende conto che la variabile è già stata dichiarata e salta semplicemente quella porzione o crea effettivamente un punto per essa ogni volta in memoria?

Può riutilizzare lo spazio che consuma la variabile e può estrarre gli invarianti dal tuo ciclo. Nel caso dell'array const char (sopra) - quell'array potrebbe essere estratto. Tuttavia, il costruttore e il distruttore devono essere eseguiti ad ogni iterazione nel caso di un oggetto (come std::string). Nel caso di std::string, quello "spazio" include un puntatore che contiene l'allocazione dinamica che rappresenta i caratteri. Così questo:

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

richiederebbe una copia ridondante in ogni caso, un'allocazione dinamica e libera se la variabile si trova al di sopra della soglia per il conteggio dei caratteri SSO (e SSO è implementato dalla libreria std).

Facendo questo:

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

richiederebbe comunque una copia fisica dei caratteri ad ogni iterazione, ma il modulo potrebbe comportare un'allocazione dinamica poiché si assegna la stringa e l'implementazione dovrebbe vedere che non è necessario ridimensionare l'allocazione di supporto della stringa. Naturalmente, in questo esempio non lo faresti (perché sono già state dimostrate più alternative superiori), ma potresti considerarlo quando il contenuto della stringa o del vettore varia.

Quindi cosa fai con tutte quelle opzioni (e altro)? Tienilo molto vicino come impostazione predefinita, fino a quando non capisci bene i costi e sai quando dovresti deviare.


1
Per quanto riguarda i tipi di dati di base come float o int, la dichiarazione della variabile all'interno del ciclo sarà più lenta rispetto alla dichiarazione di quella variabile all'esterno del ciclo in quanto dovrà allocare uno spazio per la variabile ogni iterazione?
Kasparov92,

2
@ Kasparov92 La risposta breve è "No. Ignora quell'ottimizzazione e inseriscila nel loop quando possibile per una migliore leggibilità / località. Il compilatore può eseguire quella micro-ottimizzazione per te." Più in dettaglio, ciò alla fine spetta al compilatore decidere, in base a ciò che è meglio per la piattaforma, i livelli di ottimizzazione, ecc. Un normale int / float all'interno di un loop viene solitamente posizionato nello stack. Un compilatore può certamente spostarlo al di fuori del ciclo e riutilizzare la memoria se c'è un'ottimizzazione nel farlo. Ai fini pratici, questa sarebbe un'ottimizzazione molto molto molto piccola ...
justin

1
@ Kasparov92 ... (segue) che considereresti solo in ambienti / applicazioni in cui ogni ciclo contava. In tal caso, potresti voler solo considerare l'utilizzo di assembly.
justin

14

Per C ++ dipende da cosa stai facendo. OK, è un codice stupido ma immagina

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;

}

Attenderai 55 secondi fino a quando non otterrai l'output di myFunc. Solo perché ogni contratore e distruttore ad anello richiede 5 secondi per terminare.

Avrai bisogno di 5 secondi per ottenere l'output di myOtherFunc.

Certo, questo è un esempio folle.

Ciò dimostra che potrebbe diventare un problema di prestazioni quando ogni ciclo ha la stessa costruzione quando il costruttore e / o il distruttore hanno bisogno di un po 'di tempo.


2
Bene, tecnicamente nella seconda versione otterrai l'output in soli 2 secondi, perché non hai ancora distrutto l'oggetto .....
Chrys,

12

Non ho postato per rispondere alle domande di JeremyRR (poiché hanno già ricevuto risposta); invece, ho pubblicato solo per dare un suggerimento.

A JeremyRR, potresti fare questo:

{
  string someString = "testing";   

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

  // The variable is in scope.
}

// The variable is no longer in scope.

Non so se ti rendi conto (non l'ho fatto quando ho iniziato a programmare), che le parentesi (purché siano in coppia) possono essere posizionate ovunque all'interno del codice, non solo dopo "if", "for", " while ", ecc.

Il mio codice è stato compilato in Microsoft Visual C ++ 2010 Express, quindi so che funziona; inoltre, ho provato a utilizzare la variabile al di fuori delle parentesi in cui è stata definita e ho ricevuto un errore, quindi so che la variabile è stata "distrutta".

Non so se sia una cattiva pratica usare questo metodo, poiché molte parentesi senza etichetta potrebbero rendere il codice illeggibile rapidamente, ma forse alcuni commenti potrebbero chiarire le cose.


4
Per me, questa è una risposta molto legittima che porta un suggerimento direttamente collegato alla domanda. Hai il mio voto!
Alexis Leclerc,

0

È una buona pratica, poiché tutte le risposte sopra riportate forniscono un ottimo aspetto teorico della domanda, lasciami dare un'occhiata al codice, stavo cercando di risolvere DFS su GEEKSFORGEEKS, ho riscontrato il problema dell'ottimizzazione ...... Se provi a risolvere il codice che dichiara il numero intero al di fuori del ciclo ti darà un errore di ottimizzazione.

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

Ora inserisci numeri interi all'interno del ciclo che ti daranno la risposta corretta ...

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

questo riflette completamente ciò che sir @justin stava dicendo nel secondo commento .... prova qui https://practice.geeksforgeeks.org/problems/depth-first-traversal-for-a-graph/1 . provalo ... lo capirai. Spero che questo aiuto.


Non penso che questo si applichi alla domanda. Ovviamente, nel tuo caso sopra è importante. La domanda riguardava il caso in cui la definizione della variabile potesse essere definita altrove senza modificare il comportamento del codice.
Carter

Nel codice che hai pubblicato, il problema non è la definizione ma la parte di inizializzazione. flagdovrebbe essere reinizializzato a 0 ogni whileiterazione. Questo è un problema logico, non un problema di definizione.
Martin Véronneau,

0

Capitolo 4.8 Struttura a blocchi in The C Programming Language 2.Ed. :

Una variabile automatica dichiarata e inizializzata in un blocco viene inizializzata ogni volta che il blocco viene inserito.

Potrei aver perso la descrizione rilevante nel libro come:

Una variabile automatica dichiarata e inizializzata in un blocco viene allocata una sola volta prima che il blocco venga inserito.

Ma un semplice test può provare l'assunto:

 #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;                                                                                                         
 }                                                                                                                     
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.