¿Las llaves innecesarias en C ++?


187

Al hacer una revisión de código para un colega hoy, vi una cosa peculiar. Había rodeado su nuevo código con llaves como esta:

Constructor::Constructor()
{
   existing code

   {
      New code: do some new fancy stuff here
   }

   existing code
}

¿Cuál es el resultado, si lo hay, de esto? ¿Cuál podría ser la razón para hacer esto? ¿De dónde viene este hábito?

Editar:

En base a los comentarios y algunas preguntas a continuación, siento que tengo que agregar algunas a la pregunta, a pesar de que ya marqué una respuesta.

El entorno es dispositivos integrados. Hay una gran cantidad de código C heredado envuelto en ropa C ++. Hay muchos desarrolladores convertidos en C ++.

No hay secciones críticas en esta parte del código. Solo lo he visto en esta parte del código. No se realizan asignaciones importantes de memoria, solo algunos indicadores que se configuran y algunos cambios de bits.

El código que está rodeado de llaves se parece a:

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

(No importa el código, solo quédese con las llaves ...;)) Después de las llaves hay un poco más de torsión, verificación de estado y señalización básica.

Hablé con el chico y su motivación fue limitar el alcance de las variables, nombrar enfrentamientos y otros que realmente no pude entender.

Desde mi punto de vista, esto parece bastante extraño y no creo que las llaves estén en nuestro código. Vi algunos buenos ejemplos en todas las respuestas sobre por qué uno podría rodear el código con llaves, pero ¿no debería separar el código en métodos?


99
¿Cuál fue la respuesta de tu colega cuando le preguntaste por qué lo hizo?
Graham Borland

20
Muy común con el patrón RAII. Descripción
Marcin

9
Odio las llaves innecesarias
jacknad

8
¿Hubo alguna declaración en el bloque interno?
Keith Thompson

15
tal vez solo quería 'doblar' fácilmente esa nueva sección en su editor
wim

Respuestas:


281

A veces es bueno, ya que te da un nuevo alcance, donde puedes declarar más "limpiamente" nuevas variables (automáticas).

En C++esto no es quizá tan importante ya que se puede introducir nuevas variables en cualquier lugar, pero tal vez la costumbre es de C, donde no se podía hacer esto hasta C99. :)

Dado que C++tiene destructores, también puede ser útil tener recursos (archivos, mutexes, lo que sea) liberados automáticamente a medida que sale el alcance, lo que puede hacer las cosas más limpias. Esto significa que puede conservar algún recurso compartido durante un período más corto de lo que lo haría si lo tomara al comienzo del método.


37
+1 para la mención explícita de nuevas variables y viejos hábitos
arne

46
+1 por usar el alcance de bloque utilizado para liberar recursos lo más rápido posible
Leo

9
También es fácil 'if (0)' un bloque.
vrdhn

Mis revisores de código a menudo me dicen que escribo funciones / métodos demasiado cortos. Para mantenerlos felices y para mantenerme feliz (es decir, para separar las preocupaciones no relacionadas, la localidad variable, etc.), utilizo solo esta técnica elaborada por @unwind.
ossandcad

21
@ossandcad, ¿te dicen que tus métodos son "demasiado cortos"? Eso es extremadamente difícil de hacer. El 90% de los desarrolladores (probablemente yo mismo incluido) tienen el problema opuesto.
Ben Lee

169

Un posible propósito es controlar el alcance variable . Y dado que las variables con almacenamiento automático se destruyen cuando salen del alcance, esto también puede permitir que se llame a un destructor antes de lo que lo haría de otra manera.


14
Por supuesto, realmente ese bloque debería convertirse en una función separada.
BlueRaja - Danny Pflughoeft

8
Nota histórica: Esta es una técnica del lenguaje C temprano que permitió la creación de variables locales temporales.
Thomas Matthews

12
Tengo que decir que, aunque estoy satisfecho con mi respuesta, realmente no es la mejor respuesta aquí; las mejores respuestas mencionan explícitamente RAII, ya que es la principal razón por qué te gustaría un destructor que se llamará en un punto específico. Este parece ser un caso de "arma más rápida en Occidente": publiqué lo suficientemente rápido como para obtener suficientes votos positivos iniciales que obtuve "impulso" para obtener votos más rápidos que algunas mejores respuestas. ¡No es que me esté quejando! :-)
ruakh

77
@ BlueRaja-DannyPflughoeft Estás simplificando demasiado. "Ponerlo en una función separada" no es la solución para cada problema de código. El código en uno de estos bloques podría estar estrechamente relacionado con el código circundante, tocando varias de sus variables. Usando funciones C, eso requiere operaciones de puntero. Además, no todos los fragmentos de código son (o deberían ser) reutilizables, y a veces el código puede no tener sentido por sí solo. A veces pongo bloques alrededor de mis fordeclaraciones para crear un int i;C89 de corta duración . ¿Seguramente no estás sugiriendo que todos fordeberían estar en una función separada?
Anders Sjöqvist

101

Las llaves adicionales se utilizan para definir el alcance de la variable declarada dentro de las llaves. Se hace para que se llame al destructor cuando la variable se salga del alcance. En el destructor, puede liberar un mutex (o cualquier otro recurso) para que otros puedan adquirirlo.

En mi código de producción, he escrito algo como esto:

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 puede ver, de esta manera, puede usar scoped_lock una función y, al mismo tiempo, puede definir su alcance mediante el uso de llaves adicionales. Esto garantiza que, aunque el código que se encuentra fuera de los corchetes adicionales pueda ejecutarse mediante múltiples subprocesos simultáneamente, el código dentro de los corchetes se ejecutará exactamente por un subproceso a la vez.


1
Creo que es más limpio solo tener: scoped_lock lock (mutex) // código de sección crítica y luego lock.unlock ().
chisporroteo

17
@szielenski: ¿Qué pasa si el código de la sección crítica arroja una excepción? O el mutex se bloqueará para siempre, o el código no será tan limpio como dijiste.
Nawaz

44
@Nawaz: el enfoque de @ szielenski no dejará el mutex bloqueado en caso de excepciones. También usa un scoped_lockque será destruido en las excepciones. Por lo general, prefiero introducir un nuevo alcance para el bloqueo también, pero en algunos casos unlockes muy útil. Por ejemplo, para declarar una nueva variable local dentro de la sección crítica y luego usarla más tarde. (Sé que llego tarde, pero para completar ...)
Stephan

51

Como otros han señalado, un nuevo bloque introduce un nuevo alcance, lo que le permite a uno escribir un poco de código con sus propias variables que no destruyen el espacio de nombres del código circundante, y no usa recursos más de lo necesario.

Sin embargo, hay otra buena razón para hacer esto.

Es simplemente aislar un bloque de código que logra un (sub) propósito particular. Es raro que una sola declaración logre un efecto computacional que quiero; Por lo general, se necesitan varios. Colocarlos en un bloque (con un comentario) me permite decirle al lector (a menudo a mí mismo en una fecha posterior):

  • Esta porción tiene un propósito conceptual coherente
  • Aquí está todo el código necesario
  • Y aquí hay un comentario sobre el fragmento.

p.ej

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

Podría argumentar que debería escribir una función para hacer todo eso. Si solo lo hago una vez, escribir una función solo agrega sintaxis y parámetros adicionales; Parece que no tiene mucho sentido. Solo piense en esto como una función anónima sin parámetros.

Si tiene suerte, su editor tendrá una función de plegado / desplegado que incluso le permitirá ocultar el bloque.

Hago esto todo el tiempo. Es un gran placer conocer los límites del código que necesito inspeccionar, y aún mejor saber que si ese fragmento no es el que quiero, no tengo que mirar ninguna de las líneas.


23

Una razón podría ser que la vida útil de cualquier variable declarada dentro del nuevo bloque de llaves se restringe a este bloque. Otra razón que viene a la mente es poder usar el plegado de código en el editor favorito.


17

Esto es lo mismo que un bloque if(o whileetc.), solo que sin if . En otras palabras, introduce un ámbito sin introducir una estructura de control.

Este "alcance explícito" suele ser útil en los siguientes casos:

  1. Para evitar conflictos de nombres.
  2. A alcance using.
  3. Para controlar cuándo se llaman los destructores.

Ejemplo 1:

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

// ...

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

Si my_variableresulta ser un nombre particularmente bueno para dos variables diferentes que se utilizan de forma aislada, el alcance explícito le permite evitar inventar un nuevo nombre solo para evitar el choque de nombres.

Esto también le permite evitar usar my_variable fuera de su alcance previsto por accidente.

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

}

Las situaciones prácticas en las que esto es útil son raras y pueden indicar que el código está maduro para la refactorización, pero el mecanismo está ahí si alguna vez lo necesita realmente.

Ejemplo 3:

{
    MyRaiiClass guard1 = ...;

    // ...

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

    // ...

} // ~MyRaiiClass for guard1 called.

Esto puede ser importante para RAII en los casos en que la necesidad de liberar recursos no "cae" naturalmente en los límites de las funciones o las estructuras de control.


15

Esto es realmente útil cuando se usan bloqueos de ámbito junto con secciones críticas en la programación multiproceso. Su bloqueo de ámbito inicializado en las llaves (generalmente el primer comando) quedará fuera de alcance al final del bloque y, por lo tanto, otros subprocesos podrán ejecutarse nuevamente.


14

Todos los demás ya cubrieron correctamente las posibilidades de alcance, RAII, etc., pero como mencionas un entorno incrustado, hay otra razón potencial:

Tal vez el desarrollador no confía en la asignación de registros de este compilador o quiere controlar explícitamente el tamaño del marco de la pila limitando el número de variables automáticas en el alcance a la vez.

Aquí isInitprobablemente estará en la pila:

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

Si saca las llaves, isInitse puede reservar espacio para el marco de la pila incluso después de que podría reutilizarse: si hay muchas variables automáticas con un alcance similarmente localizado, y el tamaño de su pila es limitado, eso podría ser un problema.

Del mismo modo, si su variable está asignada a un registro, salir del alcance debería proporcionar una pista clara de que el registro ahora está disponible para su reutilización. Tendría que mirar el ensamblador generado con y sin las llaves para determinar si esto hace una diferencia real (y perfilarlo, o observar el desbordamiento de la pila, para ver si esta diferencia realmente importa).


+1 buen punto, aunque estoy bastante seguro de que los compiladores modernos lo hacen bien sin intervención. (IIRC, al menos para compiladores no integrados, ignoraron la palabra clave 'registrarse' desde '99 porque siempre podían hacer un mejor trabajo que usted.)
Rup

11

Creo que otros ya han cubierto el alcance, por lo que mencionaré que las llaves innecesarias también pueden servir para el proceso de desarrollo. Por ejemplo, suponga que está trabajando en una optimización para una función existente. Cambiar la optimización o rastrear un error a una secuencia particular de declaraciones es simple para el programador; vea el comentario antes de las llaves:

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

Esta práctica es útil en ciertos contextos como la depuración, los dispositivos integrados o el código personal.


10

Estoy de acuerdo con "ruakh". Si desea una buena explicación de los diversos niveles de alcance en C, consulte esta publicación:

Varios niveles de alcance en la aplicación C

En general, el uso de "Alcance de bloque" es útil si solo desea usar una variable temporal de la que no tiene que realizar un seguimiento durante la vida útil de la llamada a la función. Además, algunas personas lo usan para que pueda usar el mismo nombre de variable en varias ubicaciones por conveniencia, aunque generalmente no es una buena idea. p.ej:

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

En este ejemplo en particular, he definido returnValue dos veces, pero dado que está solo en el alcance del bloque, en lugar del alcance de la función (es decir, el alcance de la función sería, por ejemplo, declarar returnValue justo después de int main (void)), no obtener cualquier error del compilador, ya que cada bloque es ajeno a la instancia temporal de returnValue declarada.

No puedo decir que esta sea una buena idea en general (es decir: probablemente no debería reutilizar nombres de variables repetidamente de bloque a bloque), pero en general, ahorra tiempo y le permite evitar tener que administrar valor de returnValue en toda la función.

Finalmente, tenga en cuenta el alcance de las variables utilizadas en mi ejemplo 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

Pregunta ocupada, hombre. Nunca he tenido 100 ups. ¿Qué tiene de especial esta pregunta? Buen enlace C es más valioso que C ++.
Wolfpack'08

5

Entonces, ¿por qué usar llaves "innecesarias"?

  • Para fines de "alcance" (como se mencionó anteriormente)
  • Hacer que el código sea más legible de alguna manera (más o menos como usar #pragmao definir "secciones" que se pueden visualizar)
  • Porque tú puedes. Simple como eso.

PD: no es un código MALO; Es 100% válido. Entonces, es más bien una cuestión de gusto (poco común).


5

Después de ver el código en la edición, puedo decir que los corchetes innecesarios probablemente (en la vista de los codificadores originales) estén 100% claros de lo que sucederá durante el si / entonces, aunque ahora solo sea una línea, podría ser más líneas después, y los corchetes le garantizan que no cometerá un error.

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

si lo anterior era original, y eliminar "extras" daría como resultado:

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

entonces, una modificación posterior podría verse así:

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

y eso, por supuesto, causaría un problema, ya que ahora isInit siempre se devolvería, independientemente de si / entonces.


4

Los objetos se destruyen automáticamente cuando salen del alcance ...


2

Otro ejemplo de uso son las clases relacionadas con la interfaz de usuario, especialmente Qt.

Por ejemplo, tiene una interfaz de usuario complicada y muchos widgets, cada uno de ellos tiene su propio espacio, diseño, etc. En lugar de nombrarlos space1, space2, spaceBetween, layout1, ... , puede salvarse de nombres no descriptivos para variables que existen solo en dos o tres líneas de código.

Bueno, algunos podrían decir que debería dividirlo en métodos, pero crear 40 métodos no reutilizables no se ve bien, así que decidí agregar llaves y comentarios antes de ellos, por lo que parece un bloque lógico. Ejemplo:

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

No puedo decir que sea la mejor práctica, pero es buena para el código heredado.

Obtuve estos problemas cuando muchas personas agregaron sus propios componentes a la interfaz de usuario y algunos métodos se volvieron realmente masivos, pero no es práctico crear 40 métodos de uso único dentro de la clase que ya estaban en mal estado.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.