Chaves desnecessárias em C ++?


187

Ao fazer uma revisão de código para um colega hoje, vi uma coisa peculiar. Ele havia cercado seu novo código com chaves assim:

Constructor::Constructor()
{
   existing code

   {
      New code: do some new fancy stuff here
   }

   existing code
}

Qual é o resultado disso, se houver? Qual poderia ser o motivo para fazer isso? De onde vem esse hábito?

Editar:

Com base na entrada e em algumas perguntas abaixo, sinto que tenho que adicionar algumas à pergunta, mesmo que já tenha marcado uma resposta.

O ambiente é dispositivos incorporados. Há muito código C herdado envolto em roupas C ++. Muitos desenvolvedores de C ++ se tornaram C ++.

Não há seções críticas nesta parte do código. Eu só vi isso nesta parte do código. Não há grandes alocações de memória, apenas alguns sinalizadores configurados e alguns ajustes.

O código cercado por chaves é algo como:

{
   bool isInit;
   (void)isStillInInitMode(&isInit);
   if (isInit) {
     return isInit;
   }
}

(Não se importe com o código, fique com os chavetas ...;)) Após os chavetas, há mais alguns ajustes, verificação de estado e sinalização básica.

Conversei com o cara e sua motivação era limitar o escopo de variáveis, conflitos de nomes e algumas outras que eu realmente não conseguia entender.

Do meu ponto de vista, isso parece um pouco estranho e eu não acho que o aparelho esteja no nosso código. Vi alguns bons exemplos em todas as respostas sobre por que alguém poderia colocar código entre chaves, mas você não deveria separar o código em métodos?


99
Qual foi a resposta do seu colega quando você perguntou por que ele fez isso?
Graham Borland

20
Muito comum com o padrão RAII. Visão geral rápida: c2.com/cgi/wiki?ResourceAcquisitionIsInitialization
Marcin

9
Eu odeio chaves desnecessárias
jacknad

8
Houve alguma declaração no bloco interno?
Keith Thompson

15
talvez ele só quisesse facilmente 'dobrar' essa nova seção em seu editor
quarta

Respostas:


281

Às vezes, é bom, pois fornece um novo escopo, onde você pode "limpar" de forma mais limpa novas variáveis ​​(automáticas).

Em C++isto não é talvez tão importante, pois você pode introduzir novas variáveis em qualquer lugar, mas talvez o hábito é de C, onde você não poderia fazer isso até C99. :)

Como C++possui destruidores, também pode ser útil ter recursos (arquivos, mutexes, qualquer que seja) liberados automaticamente à medida que o escopo termina, o que pode tornar as coisas mais limpas. Isso significa que você pode se apegar a algum recurso compartilhado por um período mais curto do que faria se o pegasse no início do método.


37
+1 para a menção explícita de novas variáveis e velho hábito
arne

46
+1 para usar escopo de bloco usado para recursos livres o mais rápido possível
Leo

9
Também é fácil 'se (0)' um bloco.
vrdhn

Meus revisores de código costumam me dizer que escrevo funções / métodos muito curtos. Para mantê-los felizes e para me manter feliz (ou seja, para separar preocupações não relacionadas, localidade variável etc.), eu uso apenas essa técnica, conforme elaborada por @unwind.
ossandcad

21
@ossandcad, eles dizem que seus métodos são "muito curtos"? Isso é extremamente difícil de fazer. 90% dos desenvolvedores (provavelmente eu mesmo incluído) têm o problema oposto.
Ben Lee

169

Um objetivo possível é controlar o escopo variável . E como as variáveis ​​com armazenamento automático são destruídas quando ficam fora do escopo, isso também pode permitir que um destruidor seja chamado mais cedo do que seria.


14
Obviamente, esse bloco deve ser transformado em uma função separada.
BlueRaja - Danny Pflughoeft

8
Nota histórica: Esta é uma técnica da linguagem C inicial que permitiu a criação de variáveis ​​temporárias locais.
21412 Thomas Thomass

12
Eu tenho que dizer - embora eu esteja satisfeito com minha resposta, não é realmente a melhor resposta aqui; as melhores respostas mencionam RAII explicitamente, pois é a principal razão pela qual você deseja que um destruidor seja chamado em um ponto específico. Parece um caso de "arma mais rápida no Ocidente": publiquei com rapidez suficiente para obter votos suficientes o suficiente para ganhar "impulso" para ganhar votos mais rápidos do que algumas respostas melhores. Não que eu esteja reclamando! :-)
ruach

7
@ BlueRaja-DannyPflughoeft Você é simplista demais. "Colocar em uma função separada" não é a solução para todos os problemas de código. O código em um desses blocos pode estar fortemente associado ao código circundante, tocando em várias de suas variáveis. Usando funções C, isso requer operações de ponteiro. Além disso, nem todo trecho de código é (ou deveria ser) reutilizável e, às vezes, o código pode nem fazer sentido por si próprio. Às vezes, coloco bloqueios em minhas fordeclarações para criar uma vida curta int i;no C89. Certamente você não está sugerindo que todos fordeveriam estar em uma função separada?
Anders Sjöqvist

101

As chaves extras são usadas para definir o escopo da variável declarada dentro das chaves. Isso é feito para que o destruidor seja chamado quando a variável sair do escopo. No destruidor, você pode liberar um mutex (ou qualquer outro recurso) para que outros possam adquiri-lo.

No meu código de produção, escrevi algo como isto:

void f()
{
   //some code - MULTIPLE threads can execute this code at the same time

   {
       scoped_lock lock(mutex); //critical section starts here

       //critical section code
       //EXACTLY ONE thread can execute this code at a time

   } //mutex is automatically released here

  //other code  - MULTIPLE threads can execute this code at the same time
}

Como você pode ver, dessa maneira, você pode usar scoped_lock em uma função e, ao mesmo tempo, definir seu escopo usando chaves extras. Isso garante que, mesmo que o código fora dos chavetas extras possa ser executado por vários threads simultaneamente, o código dentro dos chavetas será executado exatamente por um thread por vez.


1
Eu acho que é mais limpo apenas ter: scoped_lock lock (mutex) // código da seção crítica e lock.unlock ().
chiar

17
@szielenski: E se o código da seção crítica gerar exceção? Ou o mutex ficará bloqueado para sempre ou o código não ficará mais limpo como você disse.
Nawaz

4
@Nawaz: a abordagem de @ szielenski não deixará o mutex bloqueado em caso de exceções. Ele também usa um scoped_lockque será destruído em exceções. Normalmente, prefiro introduzir um novo escopo também para o bloqueio, mas em alguns casos isso unlocké muito útil. Por exemplo, para declarar uma nova variável local na seção crítica e usá-la mais tarde. (Eu sei que estou atrasado, mas apenas para ser completo ...)
Stephan

51

Como outros já apontaram, um novo bloco introduz um novo escopo, permitindo escrever um pouco de código com suas próprias variáveis ​​que não descartam o espaço de nomes do código circundante e não usam recursos mais do que o necessário.

No entanto, há outra boa razão para fazer isso.

É simplesmente isolar um bloco de código que atinja um (sub) propósito específico. É raro que uma única declaração atinja o efeito computacional que eu quero; geralmente leva vários. Colocar aqueles em um bloco (com um comentário) me permite dizer ao leitor (geralmente eu mesmo em uma data posterior):

  • Esse pedaço tem um objetivo conceitual coerente
  • Aqui está todo o código necessário
  • E aqui está um comentário sobre o pedaço.

por exemplo

{  // update the moving average
   i= (i+1) mod ARRAYSIZE;
   sum = sum - A[i];
   A[i] = new_value;
   sum = sum + new_value;
   average = sum / ARRAYSIZE ;  
}

Você pode argumentar que eu deveria escrever uma função para fazer tudo isso. Se eu fizer apenas uma vez, escrever uma função adiciona sintaxe e parâmetros adicionais; parece pouco sentido. Pense nisso como uma função anônima e sem parâmetros.

Se você tiver sorte, seu editor terá uma função de dobra / desdobramento que permitirá que você oculte o bloco.

Eu faço isso toda hora. É um grande prazer conhecer os limites do código que preciso inspecionar e ainda melhor saber que, se esse pedaço não for o que eu quero, não preciso olhar para nenhuma das linhas.


23

Uma razão pode ser que o tempo de vida de qualquer variável declarada dentro do novo bloco de chaves seja restrito a esse bloco. Outro motivo que vem à mente é poder usar a dobragem de código no editor favorito.


17

É o mesmo que um bloco if(ou whileetc.), apenas sem if . Em outras palavras, você introduz um escopo sem introduzir uma estrutura de controle.

Esse "escopo explícito" geralmente é útil nos seguintes casos:

  1. Para evitar conflitos de nome.
  2. Para escopo using.
  3. Controlar quando os destruidores são chamados.

Exemplo 1:

{
    auto my_variable = ... ;
    // ...
}

// ...

{
    auto my_variable = ... ;
    // ...
}

Se por my_variableacaso for um nome particularmente bom para duas variáveis ​​diferentes usadas isoladamente uma da outra, o escopo explícito permitirá evitar inventar um novo nome apenas para evitar o conflito de nomes.

Isso também permite evitar o uso my_variablefora do escopo pretendido por acidente.

Exemplo 2:

namespace N1 { class A { }; }
namespace N2 { class A { }; }

void foo() {

    {
        using namespace N1;
        A a; // N1::A.
        // ...
    }

    {
        using namespace N2;
        A a; // N2::A.
        // ...
    }

}

Situações práticas em que isso é útil são raras e podem indicar que o código está pronto para refatoração, mas o mecanismo está presente caso você realmente precise dele.

Exemplo 3:

{
    MyRaiiClass guard1 = ...;

    // ...

    {
        MyRaiiClass guard2 = ...;
        // ...
    } // ~MyRaiiClass for guard2 called.

    // ...

} // ~MyRaiiClass for guard1 called.

Isso pode ser importante para a RAII nos casos em que a necessidade de liberar recursos naturalmente não "cai" nos limites das funções ou estruturas de controle.


15

Isso é realmente útil ao usar bloqueios de escopo em conjunto com seções críticas na programação multithread. Seu bloqueio de escopo inicializado entre chaves (geralmente o primeiro comando) ficará fora do escopo no final do final do bloco e, portanto, outros threads poderão executar novamente.


14

Todo mundo já cobriu corretamente as possibilidades de escopo, RAII etc., mas como você menciona um ambiente incorporado, há um outro motivo potencial:

Talvez o desenvolvedor não confie na alocação de registro deste compilador ou queira controlar explicitamente o tamanho do quadro da pilha limitando o número de variáveis ​​automáticas no escopo de uma só vez.

Aqui isInitprovavelmente estará na pilha:

{
   bool isInit;
   (void)isStillInInitMode(&isInit);
   if (isInit) {
     return isInit;
   }
}

Se você remover os chavetas, o espaço para isInitpoderá ser reservado no quadro da pilha, mesmo depois que possa ser reutilizado: se houver muitas variáveis ​​automáticas com escopo localizado da mesma forma e o tamanho da pilha for limitado, isso pode ser um problema.

Da mesma forma, se sua variável for alocada para um registro, sair do escopo deve fornecer uma forte dica de que o registro agora está disponível para reutilização. Você teria que olhar para o assembler gerado com e sem chaves para descobrir se isso faz alguma diferença real (e criar um perfil - ou observar o estouro da pilha - para ver se essa diferença realmente importa).


+1 de um ponto positivo, embora eu tenha certeza de que os compiladores modernos acertam sem intervenção. (IIRC - para compiladores, pelo menos, não embutido - eles ignoraram a palavra-chave 'registrar' já em '99, porque eles sempre poderia fazer um trabalho melhor do que você poderia.)
Rup

11

Acho que outros já abordaram o escopo, portanto, mencionarei que os aparelhos desnecessários também podem servir a um propósito no processo de desenvolvimento. Por exemplo, suponha que você esteja trabalhando em uma otimização para uma função existente. Alternar a otimização ou rastrear um bug para uma sequência específica de instruções é simples para o programador - veja o comentário antes das chaves:

// if (false) or if (0) 
{
   //experimental optimization  
}

Essa prática é útil em certos contextos, como depuração, dispositivos incorporados ou código pessoal.


10

Eu concordo com "ruakh". Se você deseja uma boa explicação dos vários níveis de escopo em C, confira este post:

Vários níveis de escopo na aplicação C

Em geral, o uso do "Escopo do bloco" é útil se você deseja usar apenas uma variável temporária que não precisa acompanhar durante toda a vida útil da chamada de função. Além disso, algumas pessoas o usam para que você possa usar o mesmo nome de variável em vários locais por conveniência, embora isso geralmente não seja uma boa ideia. por exemplo:

int unusedInt = 1;

int main(void) {
  int k;

  for(k = 0; k<10; k++) {
    int returnValue = myFunction(k);
    printf("returnValue (int) is: %d (k=%d)",returnValue,k);
  }

  for(k = 0; k<100; k++) {
    char returnValue = myCharacterFunction(k);
    printf("returnValue (char) is: %c  (k=%d)",returnValue,k);
  }

  return 0;
}

Neste exemplo em particular, eu defini returnValue duas vezes, mas como está apenas no escopo do bloco, em vez do escopo da função (ou seja: o escopo da função seria, por exemplo, declarar returnValue logo após int main (void)), não obtenha qualquer erro do compilador, pois cada bloco está alheio à instância temporária de returnValue declarada.

Não posso dizer que essa seja uma boa ideia em geral (ou seja: você provavelmente não deve reutilizar nomes de variáveis ​​repetidamente de bloco a bloco), mas, em geral, economiza tempo e evita a necessidade de gerenciar o valor de returnValue em toda a função.

Por fim, observe o escopo das variáveis ​​usadas no meu exemplo de código:

int:  unusedInt:   File and global scope (if this were a static int, it would only be file scope)
int:  k:           Function scope
int:  returnValue: Block scope
char: returnValue: Block scope

Pergunta ocupada, cara. Eu nunca tive 100 ups. O que há de tão especial nessa questão? Bom link. C é mais valioso que C ++.
usar o seguinte

5

Então, por que usar chaves "desnecessárias"?

  • Para fins de "escopo" (como mencionado acima)
  • Tornar o código mais legível de uma maneira (praticamente como usar #pragmaou definir "seções" que podem ser visualizadas)
  • Porque você pode. Simples assim.

PS Não é um código MAU; é 100% válido. Portanto, é uma questão de gosto (incomum).


5

Depois de visualizar o código na edição, posso dizer que os colchetes desnecessários provavelmente são (na visão dos codificadores originais) 100% claros do que acontecerá durante o if / then, mesmo que seja apenas uma linha agora, pode ser mais linhas depois, e os colchetes garantem que você não cometa um erro.

{
   bool isInit;
   (void)isStillInInitMode(&isInit);
   if (isInit) {
     return isInit;
   }
   return -1;
}

se o acima foi original, e a remoção de "extras" resultará em:

{
   bool isInit;
   (void)isStillInInitMode(&isInit);
   if (isInit) 
     return isInit;
   return -1;
}

então, uma modificação posterior pode ser assim:

{
   bool isInit;
   (void)isStillInInitMode(&isInit);
   if (isInit) 
     CallSomethingNewHere();
     return isInit;
   return -1;
}

e isso, é claro, causaria um problema, já que agora isInit sempre seria retornado, independentemente do if / then.


4

Os objetos são destruídos automaticamente quando ficam fora do escopo ...


2

Outro exemplo de uso são as classes relacionadas à interface do usuário, especialmente Qt.

Por exemplo, você tem uma interface do usuário complicada e muitos widgets, cada um com seu próprio espaçamento, layout e etc. Em vez de nomeá-los, space1, space2, spaceBetween, layout1, ...você pode se salvar de nomes não descritivos para variáveis ​​que existem apenas em duas ou três linhas de código.

Bem, alguns podem dizer que você deve dividi-lo em métodos, mas a criação de 40 métodos não reutilizáveis ​​não parece ok - então eu decidi apenas adicionar chaves e comentários antes deles, para que pareça um bloco lógico. Exemplo:

// Start video button 
{ 
   <here the code goes> 
}
// Stop video button
{
   <...>
}
// Status label
{
   <...>
}

Não posso dizer que é a melhor prática, mas é boa para o código herdado.

Esses problemas surgiram quando muitas pessoas adicionaram seus próprios componentes à interface do usuário e alguns métodos se tornaram realmente massivos, mas não é prático criar 40 métodos de uso único dentro da classe que já foram uma bagunça.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.