Declarar variáveis ​​dentro de loops, boas ou más práticas?

266

Pergunta 1: declarar uma variável dentro de um loop é uma boa ou má prática?

Eu li os outros tópicos sobre se há ou não um problema de desempenho (a maioria disse não) e que você deve sempre declarar variáveis ​​o mais próximo possível de onde elas serão usadas. O que eu quero saber é se isso deve ou não ser evitado ou se é realmente preferido.

Exemplo:

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

   cout << someString;
}

Pergunta 2: A maioria dos compiladores percebe que a variável já foi declarada e apenas pula essa parte ou cria um local para ela na memória toda vez?

JeramyRR
fonte
29
Coloque-os próximos ao uso, a menos que o perfil diga o contrário.
quer
1
Aqui estão algumas perguntas semelhantes: stackoverflow.com/questions/982963/… stackoverflow.com/questions/407255/…
drnewman
3
@ drnewman Eu li esses tópicos, mas eles não responderam à minha pergunta. Entendo que declarar variáveis ​​dentro de loops funciona. Gostaria de saber se é uma boa prática fazer isso ou se é algo a ser evitado.
JeramyRR

Respostas:

348

Esta é uma excelente prática.

Ao criar variáveis ​​dentro de loops, você garante que seu escopo seja restrito a dentro do loop. Não pode ser referenciado nem chamado fora do loop.

Deste jeito:

  • Se o nome da variável for um pouco "genérico" (como "i"), não há risco de misturá-la com outra variável com o mesmo nome em algum lugar posteriormente no seu código (também pode ser mitigado usando a -Wshadowinstrução de aviso no GCC)

  • O compilador sabe que o escopo da variável está limitado ao interior do loop e, portanto, emitirá uma mensagem de erro adequada se a variável for referenciada por engano em outro local.

  • Por último, mas não menos importante, alguma otimização dedicada pode ser executada com mais eficiência pelo compilador (o mais importante é alocar o registro), pois sabe que a variável não pode ser usada fora do loop. Por exemplo, não há necessidade de armazenar o resultado para reutilização posterior.

Em suma, você está certo em fazê-lo.

Observe, no entanto, que a variável é não deve manter seu valor entre cada loop. Nesse caso, pode ser necessário inicializá-lo sempre. Você também pode criar um bloco maior, abrangendo o loop, cujo único objetivo é declarar variáveis ​​que devem manter seu valor de um loop para outro. Isso normalmente inclui o próprio contador de loop.

{
    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 a pergunta 2: A variável é alocada uma vez, quando a função é chamada. De fato, de uma perspectiva de alocação, é (quase) o mesmo que declarar a variável no início da função. A única diferença é o escopo: a variável não pode ser usada fora do loop. Pode até ser possível que a variável não esteja alocada, apenas reutilizando algum espaço livre (de outra variável cujo escopo terminou).

Com escopo restrito e preciso, otimizações mais precisas. Mais importante, porém, isso torna seu código mais seguro, com menos estados (ou seja, variáveis) com que se preocupar ao ler outras partes do código.

Isso é verdade mesmo fora de um if(){...}bloco. Normalmente, em vez de:

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

é mais seguro escrever:

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

A diferença pode parecer pequena, especialmente em um exemplo tão pequeno. Mas em uma base de código maior, ajudará: agora não há risco de transportar algum resultvalor de f1()para f2()bloquear. Cada um resulté estritamente limitado ao seu próprio escopo, tornando seu papel mais preciso. Do ponto de vista do revisor, é muito melhor, pois ele tem menos variáveis ​​de estado de longo alcance para se preocupar e acompanhar.

Até o compilador ajudará melhor: supondo que, no futuro, após alguma alteração incorreta de código, resultnão seja adequadamente inicializado f2(). A segunda versão simplesmente se recusará a trabalhar, declarando uma mensagem de erro clara em tempo de compilação (muito melhor que o tempo de execução). A primeira versão não localizará nada, o resultado de f1()simplesmente será testado uma segunda vez, sendo confundido pelo resultado de f2().

Informação complementar

A ferramenta de código aberto CppCheck (uma ferramenta de análise estática para código C / C ++) fornece algumas dicas excelentes sobre o escopo ideal das variáveis.

Em resposta ao comentário sobre alocação: A regra acima é verdadeira em C, mas pode não ser para algumas classes C ++.

Para tipos e estruturas padrão, o tamanho da variável é conhecido no momento da compilação. Não existe "construção" em C; portanto, o espaço para a variável será simplesmente alocado na pilha (sem nenhuma inicialização), quando a função é chamada. É por isso que existe um custo "zero" ao declarar a variável dentro de um loop.

No entanto, para as classes C ++, existe esse construtor sobre o qual sei muito menos. Acho que a alocação provavelmente não será o problema, pois o compilador deve ser inteligente o suficiente para reutilizar o mesmo espaço, mas é provável que a inicialização ocorra a cada iteração do loop.

Ciano
fonte
4
Resposta incrível. Era exatamente isso que eu procurava e até me deu uma ideia de algo que eu não percebi. Não percebi que o escopo permanece apenas dentro do loop. Obrigado pela resposta!
JeramyRR
22
"Mas nunca será mais lento do que alocar no início da função". Isso nem sempre é verdade. A variável será alocada uma vez, mas ainda será construída e destruída quantas vezes for necessário. Que, no caso do código de exemplo, é 11 vezes. Para citar o comentário de Mooing "Coloque-os perto de seu uso, a menos que os perfis digam o contrário".
IronMensan
4
@JeramyRR: Absolutamente não - o compilador não tem como saber se o objeto tem efeitos colaterais significativos em seu construtor ou destruidor.
Ildjarn 31/10/11
2
@ Ferro: Por outro lado, quando você declara o item primeiro, você recebe muitas chamadas para o operador de atribuição; que normalmente custa quase o mesmo que construir e destruir um objeto.
Billy ONeal
4
@ Billyilly: Para stringe vectorespecificamente, o operador de atribuição pode reutilizar o buffer alocado em cada loop, o que (dependendo do seu loop) pode ser uma enorme economia de tempo.
Mooing Duck
22

Geralmente, é uma prática muito boa mantê-lo muito próximo.

Em alguns casos, haverá uma consideração como o desempenho que justifica a retirada da variável do loop.

No seu exemplo, o programa cria e destrói a string a cada vez. Algumas bibliotecas usam uma otimização de cadeia de caracteres pequena (SSO), portanto, a alocação dinâmica pode ser evitada em alguns casos.

Suponha que você queira evitar essas criações / alocações redundantes, escreva-as como:

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

ou você pode extrair a constante:

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

A maioria dos compiladores percebe que a variável já foi declarada e apenas pula essa parte, ou ela realmente cria um ponto para ela na memória toda vez?

Ele pode reutilizar o espaço que a variável consome e pode puxar invariantes para fora do seu loop. No caso da matriz const char (acima) - essa matriz pode ser removida. No entanto, o construtor e o destruidor devem ser executados a cada iteração no caso de um objeto (como std::string). No caso de std::string, esse 'espaço' inclui um ponteiro que contém a alocação dinâmica que representa os caracteres. Então, é isso:

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

exigiria cópia redundante em cada caso e alocação dinâmica e livre se a variável ficar acima do limite para a contagem de caracteres SSO (e o SSO for implementado pela sua biblioteca std).

Fazendo isso:

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

ainda exigiria uma cópia física dos caracteres em cada iteração, mas o formulário pode resultar em uma alocação dinâmica, porque você atribui a string e a implementação deve verificar que não há necessidade de redimensionar a alocação de backup da string. Obviamente, você não faria isso neste exemplo (porque várias alternativas superiores já foram demonstradas), mas você pode considerá-lo quando a string ou o conteúdo do vetor variar.

Então, o que você faz com todas essas opções (e mais)? Mantenha-o muito próximo como padrão - até você entender bem os custos e saber quando deve se desviar.

justin
fonte
1
Em relação aos tipos de dados básicos, como float ou int, declarar a variável dentro do loop será mais lento do que declarar essa variável fora do loop, pois será necessário alocar um espaço para a variável a cada iteração?
Kasparov92
2
@ Kasparov92 A resposta curta é "Não. Ignore essa otimização e coloque-a no loop sempre que possível para melhorar a legibilidade / localidade. O compilador pode executar essa micro-otimização para você." Mais detalhadamente, isso é o que o compilador decide, com base no que é melhor para a plataforma, níveis de otimização etc. Um int / float comum dentro de um loop geralmente será colocado na pilha. Um compilador certamente pode movê-lo para fora do loop e reutilizar o armazenamento se houver uma otimização para isso. Para fins práticos, isso seria uma otimização muito muito muito pequena ...
justin
1
@ Kasparov92… (continuação) que você consideraria apenas em ambientes / aplicativos em que todos os ciclos contavam. Nesse caso, você pode considerar apenas usar o assembly.
justin
14

Para C ++, depende do que você está fazendo. OK, é um código estúpido, mas imagine

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;

}

Você esperará 55 segundos até obter a saída do myFunc. Só porque cada construtor e destruidor de loop juntos precisam de 5 segundos para terminar.

Você precisará de 5 segundos até obter a saída do myOtherFunc.

Claro, este é um exemplo louco.

Mas ilustra que pode se tornar um problema de desempenho quando cada loop é feito da mesma construção quando o construtor e / ou destruidor precisa de algum tempo.

Nobby
fonte
2
Bem, tecnicamente, na segunda versão, você obterá a saída em apenas 2 segundos, porque ainda não destruiu o objeto .....
Chrys /
12

Não postei para responder às perguntas de JeremyRR (como elas já foram respondidas); em vez disso, postei apenas para dar uma sugestão.

Para o JeremyRR, você pode fazer o seguinte:

{
  string someString = "testing";   

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

  // The variable is in scope.
}

// The variable is no longer in scope.

Não sei se você percebeu (não sabia quando comecei a programar) que colchetes (desde que estejam em pares) podem ser colocados em qualquer lugar do código, não apenas depois de "se", "para", " enquanto ", etc.

Meu código compilado no Microsoft Visual C ++ 2010 Express, então eu sei que funciona; além disso, tentei usar a variável fora dos colchetes em que ela estava definida e recebi um erro, por isso sei que a variável foi "destruída".

Não sei se é uma má prática usar esse método, pois muitos colchetes não rotulados podem rapidamente tornar o código ilegível, mas talvez alguns comentários possam esclarecer as coisas.

Fearnbuster
fonte
4
Para mim, esta é uma resposta muito legítima que traz uma sugestão diretamente ligada à pergunta. Você tem o meu voto!
Alexis Leclerc
0

É uma prática muito boa, pois todas as respostas acima fornecem um aspecto teórico muito bom da pergunta, deixe-me dar uma idéia do código. Estava tentando resolver o DFS sobre GEEKSFORGEEKS, encontrei o problema de otimização ...... Se você tentar resolver o código que declara o número inteiro fora do loop fornece erro de otimização.

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

Agora coloque números inteiros dentro do loop, para obter a resposta correta ...

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

isso reflete completamente o que o senhor @justin estava dizendo no segundo comentário .... tente isso aqui https://practice.geeksforgeeks.org/problems/depth-first-traversal-for-a-graph/1 . basta dar uma chance ... você vai conseguir. Espero essa ajuda.

KhanJr
fonte
Eu não acho que isso se aplica à questão. Obviamente, no seu caso acima, importa. A questão era lidar com o caso em que a definição de variável poderia ser definida em outro lugar sem alterar o comportamento do código.
pcarter 7/01
No código que você postou, o problema não é a definição, mas a parte de inicialização. flagdeve ser reinicializado em 0 a cada whileiteração. Esse é um problema lógico, não um problema de definição.
Martin Véronneau
0

Capítulo 4.8 Estrutura de blocos na K & R's The C Programming Language 2.Ed. :

Uma variável automática declarada e inicializada em um bloco é inicializada cada vez que o bloco é inserido.

Eu poderia ter deixado de ver a descrição relevante no livro, como:

Uma variável automática declarada e inicializada em um bloco é alocada apenas uma vez antes de o bloco ser inserido.

Mas um teste simples pode provar a suposição:

 #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
fonte