¿Cuál es el propósito de usar llaves (es decir, {}) para un bucle if o de una sola línea?

Resuelto JAN asked hace 12 años • 24 respuestas

Estoy leyendo algunas notas de mi profesor de C++ y él escribió lo siguiente:

  1. Usar sangría // Aceptar
  2. Nunca confíe en la precedencia de operadores: utilice siempre paréntesis // OK
  3. Utilice siempre un bloque {}, incluso para una sola línea // no está bien , ¿por qué?
  4. Objeto constante en el lado izquierdo de la comparación // OK
  5. Utilice unsigned para variables que sean >= 0 // buen truco
  6. Establezca el puntero en NULL después de la eliminación: protección de doble eliminación // no está mal

La tercera técnica no me queda clara: ¿qué ganaría colocando una línea en un { ... }?

Por ejemplo, tome este código extraño:

int j = 0;
for (int i = 0 ; i < 100 ; ++i)
{
    if (i % 2 == 0)
    {
        j++;
    }
}

y reemplácelo con:

int j = 0;
for (int i = 0 ; i < 100 ; ++i)
    if (i % 2 == 0)
        j++;

¿Cuál es el beneficio de usar la primera versión?

JAN avatar Aug 30 '12 15:08 JAN
Aceptado

Intentemos modificar también icuando incrementamos j:

int j = 0;
for (int i = 0 ; i < 100 ; ++i)
    if (i % 2 == 0)
        j++;
        i++;

¡Oh, no! Viniendo de Python, esto parece estar bien, pero en realidad no lo es, ya que es equivalente a:

int j = 0;
for (int i = 0 ; i < 100 ; ++i)
    if (i % 2 == 0)
        j++;
i++;

Por supuesto, esto es un error tonto, pero que incluso un programador experimentado podría cometer.

Otra muy buena razón se señala en la respuesta de ta.speot.is .

Un tercero que se me ocurre es el de nested if:

if (cond1)
   if (cond2) 
      doSomething();

Ahora, suponga que desea saber doSomethingElse()cuándo cond1no se cumple (nueva característica). Entonces:

if (cond1)
   if (cond2) 
      doSomething();
else
   doSomethingElse();

lo cual es obviamente incorrecto, ya que se elseasocia con lo interno if.


Editar: dado que esto está recibiendo cierta atención, aclararé mi punto de vista. La pregunta que estaba respondiendo es:

¿Cuál es el beneficio de usar la primera versión?

que he descrito. Hay algunos beneficios. Pero, en mi opinión, las reglas de "siempre" no siempre se aplican. Así que no lo apoyo totalmente

Utilice siempre un bloque { }, incluso para una sola línea // no está bien, ¿por qué?

No estoy diciendo que siempre uses un {}bloque. Si se trata de una condición y un comportamiento bastante simples, no lo hagas. Si sospecha que alguien podría entrar más tarde y cambiar su código para agregar funcionalidad, hágalo.

Luchian Grigore avatar Aug 30 '2012 08:08 Luchian Grigore

Es muy fácil cambiar accidentalmente el flujo de control con comentarios si no usas {y }. Por ejemplo:

if (condition)
  do_something();
else
  do_something_else();

must_always_do_this();

Si comentas do_something_else()con un comentario de una sola línea, terminarás con esto:

if (condition)
  do_something();
else
  //do_something_else();

must_always_do_this();

Se compila, pero must_always_do_this()no siempre se llama.

Tuvimos este problema en nuestra base de código, donde alguien había deshabilitado algunas funciones muy rápidamente antes del lanzamiento. Afortunadamente lo detectamos en la revisión del código.

ta.speot.is avatar Aug 30 '2012 08:08 ta.speot.is

Tengo mis dudas sobre la competencia del profesor. Considerando sus puntos:

  1. DE ACUERDO
  2. ¿ Alguien realmente escribiría (o querría leer) (b*b) - ((4*a)*c)? Algunas prioridades son obvias (o deberían serlo), y los paréntesis adicionales sólo aumentan la confusión. (Por otro lado, _deberías_ usar los paréntesis en casos menos obvios, incluso si sabes que no son necesarios).
  3. Algo así como. Existen dos convenciones muy extendidas para formatear condicionales y bucles:
    si (cond) {
        código;
    }
    
    y:
    si (cont.)
    {
        código;
    }
    
    En lo primero estoy de acuerdo con él. La apertura {no es tan visible, por lo que es mejor asumir que siempre está ahí. En el segundo, sin embargo, yo (y la mayoría de las personas con las que he trabajado) no tenemos ningún problema en omitir las llaves en una sola declaración. (Siempre que, por supuesto, la sangría sea sistemática y que se utilice este estilo de forma coherente. (Y muchos muy buenos programadores, que escriben código muy legible, omiten las llaves incluso al formatear de la primera forma).
  4. NO . Cosas como if ( NULL == ptr )son lo suficientemente feas como para dificultar la legibilidad. Escribe las comparaciones de forma intuitiva. (Lo que en muchos casos da como resultado la constante de la derecha). Su 4 es un mal consejo; cualquier cosa que haga que el código no sea natural lo hace menos legible.
  5. NO . Todo lo contrario intestá reservado para casos especiales. Para programadores experimentados en C y C++, el uso de unsignedoperadores de bits de señales. C++ no tiene un tipo cardinal real (ni ningún otro tipo de subrango efectivo); unsignedno funciona para valores numéricos, debido a las reglas de promoción. Los valores numéricos en los que ninguna operación aritmética tendría sentido, como los números de serie, presumiblemente podrían ser unsigned. Yo estaría en contra, sin embargo, porque envía el mensaje equivocado: las operaciones bit a bit tampoco tienen sentido. La regla básica es que los tipos integrales son int, _a menos_ que exista una razón importante para utilizar otro tipo.
  6. NO . Hacer esto sistemáticamente es engañoso y en realidad no protege contra nada. En código OO estricto, delete this;suele ser el caso más frecuente (y no se puede configurar thisen NULL) y, de lo contrario, la mayoría deleteestán en destructores, por lo que no se puede acceder al puntero más adelante de todos modos. Y configurarlo en NULLno hace nada con respecto a otros punteros que flotan por ahí. Configurar el puntero sistemáticamente en NULLda una falsa sensación de seguridad y realmente no le compra nada.

Mira el código en cualquiera de las referencias típicas. Stroustrup viola todas las reglas que usted ha dado excepto la primera, por ejemplo.

Le sugiero que busque otro profesor. Uno que realmente sepa de lo que habla.

James Kanze avatar Aug 30 '2012 09:08 James Kanze

Todas las demás respuestas defienden la regla 3 de su profesor.

Déjame decirte que estoy de acuerdo contigo: la norma es redundante y no la recomendaría. Es cierto que, en teoría , evita errores si siempre agrega llaves. Por otro lado, nunca me he encontrado con este problema en la vida real : al contrario de lo que implican otras respuestas, ni una sola vez me olvidé de agregar las llaves una vez que se volvieron necesarias. Si usa la sangría adecuada, resulta inmediatamente obvio que necesita agregar llaves una vez que se sangra más de una declaración.

La respuesta del componente 10 destaca en realidad el único caso imaginable en el que esto realmente podría conducir a un error. Pero, por otro lado, reemplazar el código mediante expresiones regulares siempre requiere mucho cuidado.

Ahora veamos el otro lado de la medalla: ¿hay alguna desventaja en usar siempre llaves? Las otras respuestas simplemente ignoran este punto. Pero hay una desventaja: ocupa mucho espacio vertical en la pantalla y esto, a su vez, puede hacer que el código sea ilegible porque significa que tienes que desplazarte más de lo necesario.

Considere una función con muchas cláusulas de protección al principio (y sí, lo siguiente es un código C++ incorrecto, pero en otros lenguajes esto sería una situación bastante común):

void some_method(obj* a, obj* b)
{
    if (a == nullptr)
    {
        throw null_ptr_error("a");
    }
    if (b == nullptr)
    {
        throw null_ptr_error("b");
    }
    if (a == b)
    {
        throw logic_error("Cannot do method on identical objects");
    }
    if (not a->precondition_met())
    {
        throw logic_error("Precondition for a not met");
    }

    a->do_something_with(b);
}

Este es un código horrible y sostengo firmemente que el siguiente es mucho más legible:

void some_method(obj* a, obj* b)
{
    if (a == nullptr)
        throw null_ptr_error("a");
    if (b == nullptr)
        throw null_ptr_error("b");
    if (a == b)
        throw logic_error("Cannot do method on identical objects");
    if (not a->precondition_met())
        throw logic_error("Precondition for a not met");

    a->do_something_with(b);
}

De manera similar, los bucles anidados cortos se benefician al omitir las llaves:

matrix operator +(matrix const& a, matrix const& b) {
    matrix c(a.w(), a.h());

    for (auto i = 0; i < a.w(); ++i)
        for (auto j = 0; j < a.h(); ++j)
            c(i, j) = a(i, j) + b(i, j);

    return c;
}

Comparar con:

matrix operator +(matrix const& a, matrix const& b) {
    matrix c(a.w(), a.h());

    for (auto i = 0; i < a.w(); ++i)
    {
        for (auto j = 0; j < a.h(); ++j)
        {
            c(i, j) = a(i, j) + b(i, j);
        }
    }

    return c;
}

El primer código es conciso; el segundo código está inflado.

Y sí, esto se puede mitigar hasta cierto punto poniendo la llave de apertura en la línea anterior. Entonces: si insistes en usar llaves, al menos coloca la llave de apertura en la línea anterior.

En resumen: no escriba código innecesario que ocupe espacio en la pantalla.


En el tiempo transcurrido desde que escribí originalmente la respuesta, he aceptado principalmente el estilo de código predominante y uso llaves a menos que pueda poner la declaración completa en la línea anterior. Sigo manteniendo que no usar llaves redundantes suele ser más legible y todavía nunca he encontrado un error causado por esto.

Konrad Rudolph avatar Aug 30 '2012 12:08 Konrad Rudolph