Parentesi graffe non necessarie in C ++?


187

Oggi, quando ho fatto una revisione del codice per un collega, ho visto qualcosa di strano. Aveva circondato il suo nuovo codice con parentesi graffe come questa:

Constructor::Constructor()
{
   existing code

   {
      New code: do some new fancy stuff here
   }

   existing code
}

Qual è il risultato, se presente, da questo? Quale potrebbe essere la ragione per farlo? Da dove viene questa abitudine?

Modificare:

In base all'input e ad alcune domande riportate di seguito, ritengo di dover aggiungere un po 'alla domanda, anche se ho già contrassegnato una risposta.

L'ambiente è dispositivi integrati. C'è un sacco di codice C legacy avvolto in abiti C ++. Ci sono molti sviluppatori C ++ trasformati in C.

Non ci sono sezioni critiche in questa parte del codice. L'ho visto solo in questa parte del codice. Non ci sono allocazioni di memoria importanti fatte, solo alcuni flag che sono impostati e alcuni twiddling.

Il codice che è circondato da parentesi graffe è qualcosa di simile:

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

(Non importa il codice, basta attenersi alle parentesi graffe ...;)) Dopo le parentesi graffe ci sono alcuni più twiddling, controllo dello stato e segnalazione di base.

Ho parlato con il ragazzo e la sua motivazione è stata quella di limitare la portata delle variabili, gli scontri con i nomi e alcuni altri che non potevo davvero capire.

Dal mio POV questo sembra piuttosto strano e non penso che le parentesi graffe dovrebbero essere nel nostro codice. Ho visto alcuni buoni esempi in tutte le risposte sul perché si potrebbe circondare il codice con parentesi graffe, ma non si dovrebbe separare il codice in metodi invece?


99
Qual è stata la risposta del tuo collega quando gli hai chiesto perché lo ha fatto?
Graham Borland,

20
Abbastanza comune con il modello RAII. Panoramica rapida: c2.com/cgi/wiki?ResourceAcquisitionIsInitialization
Marcin

9
Odio inutili parentesi graffe
jacknad

8
Ci sono state dichiarazioni nel blocco interno?
Keith Thompson,

15
forse voleva semplicemente "ripiegare" facilmente quella nuova sezione del suo editore
wim

Risposte:


281

A volte è bello poiché ti offre un nuovo ambito, in cui puoi dichiarare in modo più "pulito" nuove variabili (automatiche).

In C++questo forse non è così importante poiché puoi introdurre nuove variabili ovunque, ma forse l'abitudine è C, da dove non puoi farlo fino a C99. :)

Dato che C++ha distruttori, può anche essere utile avere risorse (file, mutex, qualunque cosa) rilasciate automaticamente all'uscita dall'ambito, il che può rendere le cose più pulite. Ciò significa che puoi trattenere una risorsa condivisa per una durata inferiore rispetto a quella che avresti se la prendessi all'inizio del metodo.


37
+1 per la menzione esplicita di nuove variabili e vecchia abitudine
arne

46
+1 per l'utilizzo dell'ambito del blocco utilizzato per liberare risorse il più rapidamente possibile
Leone

9
È anche facile 'se (0)' un blocco.
vrdhn,

I miei revisori del codice spesso mi dicono che scrivo funzioni / metodi troppo brevi. Per renderli felici e per mantenermi felice (cioè per separare preoccupazioni non correlate, località variabili ecc.), Utilizzo solo questa tecnica elaborata da @unwind.
ossandcad,

21
@ossandcad, ti dicono che i tuoi metodi sono "troppo brevi"? È estremamente difficile da fare. Il 90% degli sviluppatori (anch'io probabilmente incluso) ha il problema opposto.
Ben Lee,

169

Uno scopo possibile è controllare l'ambito variabile . E poiché le variabili con archiviazione automatica vengono distrutte quando escono dall'ambito, ciò può anche consentire a un distruttore di essere chiamato prima di quanto altrimenti farebbe.


14
Ovviamente, quel blocco dovrebbe essere trasformato in una funzione separata.
BlueRaja - Danny Pflughoeft

8
Nota storica: questa è una tecnica del primo linguaggio C che ha permesso la creazione di variabili temporanee locali.
Thomas Matthews,

12
Devo dire che, anche se sono soddisfatto della mia risposta, non è proprio la risposta migliore qui; le risposte migliori menzionano esplicitamente RAII, poiché è la ragione principale per cui si vorrebbe chiamare un distruttore in un punto specifico. Questo sembra un caso di "pistola più veloce in Occidente": ho pubblicato abbastanza velocemente da ottenere abbastanza voti anticipati da guadagnare "slancio" per ottenere voti più velocemente di alcune risposte migliori. Non che mi lamenti! :-)
ruakh

7
@ BlueRaja-DannyPflughoeft Stai semplificando troppo. "Mettilo in una funzione separata" non è la soluzione per ogni problema di codice. Il codice in uno di questi blocchi potrebbe essere strettamente accoppiato al codice circostante, toccando molte delle sue variabili. Utilizzando le funzioni C, ciò richiede operazioni con il puntatore. Inoltre, non tutti gli snippet di codice sono (o dovrebbero essere) riutilizzabili, e talvolta il codice potrebbe non avere nemmeno senso da solo. A volte metto blocchi attorno alle mie fordichiarazioni per creare una vita breve int i;in C89. Sicuramente non stai suggerendo che ognuno fordovrebbe avere una funzione separata?
Anders Sjöqvist,

101

Le parentesi graffe aggiuntive vengono utilizzate per definire l'ambito della variabile dichiarata all'interno delle parentesi graffe. Viene fatto in modo che il distruttore venga chiamato quando la variabile non rientra nell'ambito. Nel distruttore, puoi rilasciare un mutex (o qualsiasi altra risorsa) in modo che altri possano acquisirlo.

Nel mio codice di produzione, ho scritto qualcosa del genere:

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
}

Come puoi vedere, in questo modo, puoi usarlo scoped_lock in una funzione e allo stesso tempo puoi definirne l'ambito usando parentesi graffe aggiuntive. Questo assicura che anche se il codice esterno alle parentesi graffe aggiuntive può essere eseguito contemporaneamente da più thread, il codice all'interno delle parentesi verrà eseguito esattamente da un thread alla volta.


1
Penso che sia più pulito avere: scoped_lock lock (mutex) // codice di sezione critica quindi lock.unlock ().
sfrigola il

17
@szielenski: cosa succede se il codice della sezione critica genera un'eccezione? O il mutex sarà bloccato per sempre, o il codice non sarà più pulito come hai detto.
Nawaz,

4
@Nawaz: l'approccio di @ szielenski non lascerà bloccato il mutex in caso di eccezioni. Usa anche un oggetto scoped_lockche verrà distrutto su eccezioni. Di solito preferisco introdurre un nuovo ambito anche per il blocco, ma in alcuni casi unlockè molto utile. Ad esempio, per dichiarare una nuova variabile locale all'interno della sezione critica e utilizzarla in seguito. (So ​​di essere in ritardo, ma solo per completezza ...)
Stephan,

51

Come altri hanno sottolineato, un nuovo blocco introduce un nuovo ambito, consentendo a uno di scrivere un po 'di codice con le proprie variabili che non eliminano lo spazio dei nomi del codice circostante e non usano le risorse più del necessario.

Tuttavia, c'è un'altra buona ragione per farlo.

È semplicemente isolare un blocco di codice che raggiunge uno scopo (secondario) particolare. È raro che una singola affermazione ottenga un effetto computazionale che desidero; di solito ci vogliono diversi. Collocarli in un blocco (con un commento) mi permette di dire al lettore (spesso me stesso in un secondo momento):

  • Questo pezzo ha uno scopo concettuale coerente
  • Ecco tutto il codice necessario
  • Ed ecco un commento sul pezzo.

per esempio

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

Potresti sostenere che dovrei scrivere una funzione per fare tutto ciò. Se lo faccio solo una volta, la scrittura di una funzione aggiunge solo sintassi e parametri aggiuntivi; sembra poco utile. Pensa a questa come una funzione anonima senza parametri.

Se sei fortunato, il tuo editor avrà una funzione fold / unfold che ti permetterà anche di nascondere il blocco.

Lo faccio sempre. È un grande piacere conoscere i limiti del codice che devo controllare, e ancora meglio sapere che se quel pezzo non è quello che voglio, non devo guardare nessuna delle righe.


23

Un motivo potrebbe essere che la durata di qualsiasi variabile dichiarata all'interno del nuovo blocco parentesi graffe è limitata a questo blocco. Un altro motivo che viene in mente è quello di poter usare la piegatura del codice nell'editor preferito.


17

È lo stesso di un blocco if(o whileecc.), Solo senza if . In altre parole, si introduce un ambito senza introdurre una struttura di controllo.

Questo "ambito esplicito" è in genere utile nei seguenti casi:

  1. Per evitare scontri con i nomi.
  2. Per ambito using.
  3. Per controllare quando vengono chiamati i distruttori.

Esempio 1:

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

// ...

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

Se my_variablesembra essere un nome particolarmente valido per due diverse variabili che vengono utilizzate separatamente l'una dall'altra, l'ambito esplicito consente di evitare di inventare un nuovo nome solo per evitare lo scontro tra nomi.

Ciò consente inoltre di evitare l'uso my_variableaccidentale fuori dal campo di applicazione previsto.

Esempio 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.
        // ...
    }

}

Le situazioni pratiche in cui ciò è utile sono rare e possono indicare che il codice è maturo per il refactoring, ma il meccanismo è lì se mai ne avessi veramente bisogno.

Esempio 3:

{
    MyRaiiClass guard1 = ...;

    // ...

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

    // ...

} // ~MyRaiiClass for guard1 called.

Questo può essere importante per RAII nei casi in cui la necessità di liberare risorse non cade naturalmente sui confini di funzioni o strutture di controllo.


15

Ciò è molto utile quando si utilizzano blocchi con ambito in combinazione con sezioni critiche nella programmazione multithread. Il blocco con ambito inizializzato tra parentesi graffe (in genere il primo comando) uscirà dall'ambito alla fine della fine del blocco e quindi altri thread saranno in grado di eseguire nuovamente.


14

Tutti gli altri hanno già coperto correttamente le possibilità di scoping, RAII ecc., Ma poiché si menziona un ambiente incorporato, c'è un ulteriore potenziale motivo:

Forse lo sviluppatore non si fida dell'allocazione del registro di questo compilatore o vuole controllare esplicitamente le dimensioni del frame dello stack limitando il numero di variabili automatiche nell'ambito immediatamente.

Qui isInitsarà probabilmente in pila:

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

Se si rimuovono le parentesi graffe, lo spazio per isInitpuò essere riservato nel frame dello stack anche dopo che potrebbe essere potenzialmente riutilizzato: se ci sono molte variabili automatiche con ambito localizzato in modo simile e le dimensioni dello stack sono limitate, ciò potrebbe costituire un problema.

Allo stesso modo, se la variabile è allocata in un registro, uscire dall'ambito dovrebbe fornire un forte suggerimento che il registro è ora disponibile per il riutilizzo. Dovresti guardare l'assemblatore generato con e senza le parentesi graffe per capire se questo fa davvero la differenza (e profilarlo - o guardare lo stack overflow - per vedere se questa differenza conta davvero).


+1 buon punto, anche se sono abbastanza sicuro che i compilatori moderni ottengano questo diritto senza intervento. (IIRC - almeno per i compilatori non incorporati - hanno ignorato la parola chiave 'register' già nel '99 perché potevano sempre fare un lavoro migliore di quello che potevi.)
Rup

11

Penso che altri abbiano già coperto l'ambito, quindi menzionerò le parentesi non necessarie che potrebbero servire allo scopo nel processo di sviluppo. Ad esempio, supponiamo di lavorare su un'ottimizzazione per una funzione esistente. Attivare l'ottimizzazione o tracciare un bug in una particolare sequenza di istruzioni è semplice per il programmatore - vedi il commento prima delle parentesi graffe:

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

Questa pratica è utile in alcuni contesti come debug, dispositivi incorporati o codice personale.


10

Sono d'accordo con "ruakh". Se vuoi una buona spiegazione dei vari livelli di ambito in C, dai un'occhiata a questo post:

Vari livelli di ambito in applicazione C.

In generale, l'uso di "Block scope" è utile se si desidera semplicemente utilizzare una variabile temporanea di cui non è necessario tenere traccia per tutta la durata della chiamata di funzione. Inoltre, alcune persone lo usano in modo da poter utilizzare lo stesso nome di variabile in più posizioni per comodità, anche se non è generalmente una buona idea. per esempio:

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

In questo esempio specifico, ho definito returnValue due volte, ma poiché è solo nell'ambito del blocco, anziché nell'ambito della funzione (ovvero: l'ambito della funzione sarebbe, ad esempio, dichiarare returnValue subito dopo int main (void)), non ottenere eventuali errori del compilatore, poiché ogni blocco ignora l'istanza temporanea di returnValue dichiarata.

Non posso dire che questa sia una buona idea in generale (cioè: probabilmente non dovresti riutilizzare ripetutamente i nomi delle variabili da blocco a blocco), ma in generale, risparmia tempo e ti consente di evitare di dover gestire il valore di returnValue nell'intera funzione.

Infine, tieni presente l'ambito delle variabili utilizzate nel mio esempio di codice:

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

Domanda occupata, amico. Non ho mai avuto 100 up. Cosa c'è di così speciale in questa domanda? Buon collegamento. C è più prezioso di C ++.
Wolfpack'08,

5

Quindi, perché usare parentesi graffe "non necessarie"?

  • Per scopi di "scoping" (come menzionato sopra)
  • Rendere il codice più leggibile in un modo (più o meno come usare #pragmao definire "sezioni" che possono essere visualizzate)
  • Perché tu puoi. Semplice come quella.

PS Non è un codice MALE; è valido al 100%. Quindi, è piuttosto una questione di gusti (non comuni).


5

Dopo aver visualizzato il codice nella modifica, posso dire che le parentesi non necessarie sono probabilmente (nella vista dei programmatori originali) chiare al 100% cosa accadrà durante l'if / allora, anche se ora è solo una riga, potrebbe essere più righe in seguito e le parentesi ti garantiscono di non commettere errori.

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

se quanto sopra era originale e la rimozione di "extra" causerebbe:

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

quindi, una modifica successiva potrebbe apparire così:

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

e questo, ovviamente, causerebbe un problema, poiché ora isInit verrebbe sempre restituito, indipendentemente dall'if / allora.


4

Gli oggetti vengono distrutti automagicamente quando escono dal campo di applicazione ...


2

Un altro esempio di utilizzo sono le classi correlate all'interfaccia utente, in particolare Qt.

Ad esempio, hai un'interfaccia utente complicata e molti widget, ognuno di essi ha la sua spaziatura, il layout e così via. Invece di nominarli space1, space2, spaceBetween, layout1, ...puoi salvarti da nomi non descrittivi per variabili che esistono solo in due-tre righe di codice.

Bene, alcuni potrebbero dire che dovresti dividerlo in metodi, ma la creazione di 40 metodi non riutilizzabili non sembra ok - quindi ho deciso di aggiungere solo parentesi graffe e commenti, quindi sembra un blocco logico. Esempio:

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

Non posso dire che sia la migliore pratica, ma è buona per il codice legacy.

Ho avuto questi problemi quando molte persone hanno aggiunto i loro componenti all'interfaccia utente e alcuni metodi sono diventati davvero enormi, ma non è pratico creare 40 metodi di utilizzo una tantum all'interno della classe che hanno già incasinato.

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.