No intente exprimir tantas operaciones como sea posible en una línea

No intente exprimir tantas operaciones como sea posible en una línea

El fragmento está tomado de Godot Engine proyecto. El error se detecta mediante el siguiente diagnóstico:V567 Comportamiento indefinido. La variable 't' se modifica mientras se usa dos veces entre puntos de secuencia.

static real_t out(real_t t, real_t b, real_t c, real_t d)
{
  return c * ((t = t / d - 1) * t * t + 1) + b;
}

Explicación

A veces, puede encontrar fragmentos de código en los que los autores intentan exprimir la mayor cantidad de lógica posible en un pequeño volumen de código, por medio de construcciones complejas. Esta práctica apenas ayuda al compilador, pero hace que el código sea más difícil de leer y comprender para otros programadores (o incluso para los propios autores). Además, el riesgo de cometer errores en dicho código también es mucho mayor.

Es en tales fragmentos, donde los programadores intentan poner mucho código en unas pocas líneas, donde generalmente se encuentran errores relacionados con un comportamiento indefinido. Por lo general, tienen que ver con escribir y leer de una y la misma variable dentro de un punto de secuencia. Para una mejor comprensión del problema, necesitamos discutir con más detalle las nociones de "comportamiento indefinido" y "punto de secuencia".

El comportamiento indefinido es propiedad de algunos lenguajes de programación para emitir un resultado que depende de la implementación del compilador o de los cambios de optimización. Algunos casos de comportamiento indefinido (incluido el que se analiza aquí) están estrechamente relacionados con la noción de "punto de secuencia".

Un punto de secuencia define cualquier punto en la ejecución de un programa de computadora en el que se garantiza que se habrán realizado todos los efectos secundarios de las evaluaciones anteriores y que aún no se han revelado efectos secundarios de las evaluaciones posteriores. En los lenguajes de programación C/C++ existen los siguientes puntos de secuencia:

  • puntos de secuencia para los operadores “&&”, “||”, “,”. Cuando no están sobrecargados, estos operadores garantizan un orden de ejecución de izquierda a derecha;
  • punto de secuencia para el operador ternario “?:”;
  • punto de secuencia al final de cada expresión completa (generalmente marcado con ';');
  • punto de secuencia en lugar de la llamada a la función, pero después de evaluar los argumentos;
  • punto de secuencia al regresar de la función.

Nota. El nuevo estándar de C++ ha descartado la noción de un "punto de secuencia", pero usaremos la explicación anterior para que aquellos que no están familiarizados con el tema capten la idea general de manera más fácil y rápida. Esta explicación es más simple que la nueva y es suficiente para que entendamos por qué no se deben juntar muchas operaciones en una sola "pila".

En el ejemplo con el que comenzamos, no hay ninguno de los puntos de secuencia mencionados anteriormente, mientras que el operador '=', así como los paréntesis, no pueden tratarse como tales. Por lo tanto, no podemos saber qué valor de la t se utilizará la variable al evaluar el valor devuelto.

En otras palabras, esta expresión es un solo punto de secuencia, por lo que se desconoce en qué orden t se accederá a la variable. Por ejemplo, la subexpresión "t * t" se puede evaluar antes o después de escribir en la variable "t =t / d – 1".

Código correcto

static real_t out(real_t t, real_t b, real_t c, real_t d)
{
  t = t / d - 1;
  return c * (t * t * t + 1) + b;
}

Recomendación

Obviamente no era una buena idea tratar de encajar toda la expresión en una sola línea. Además de ser difícil de leer, también facilitó que se colara un error.

Habiendo solucionado el defecto y dividido la expresión en dos partes, hemos resuelto 2 problemas a la vez:hicimos que el código fuera más legible y nos deshicimos del comportamiento indefinido al agregar un punto de secuencia.

El código discutido anteriormente no es el único ejemplo, por supuesto. Aquí hay otro:

*(mem+addr++) = 
   (opcode >= BENCHOPCODES) ? 0x00 : ((addr >> 4)+1) << 4;

Al igual que en el caso anterior, el error en este código ha sido causado por un código excesivamente complicado. El intento del programador de incrementar la dirección variable dentro de una expresión ha llevado a un comportamiento indefinido ya que se desconoce qué valor tiene la dirección variable tendrá en la parte derecha de la expresión – la original o la incrementada.

La mejor solución a este problema es la misma que antes:no complique las cosas sin razón; organice las operaciones en varias expresiones en lugar de ponerlas todas en una:

*(mem+addr) = (opcode >= BENCHOPCODES) ? 0x00 : ((addr >> 4)+1) << 4; 
addr++;

Hay una conclusión simple pero útil que se puede sacar de todo esto:no intente incluir un conjunto de operaciones en tan pocas líneas como sea posible. Puede ser preferible dividir el código en varios fragmentos, haciéndolo así más comprensible y reduciendo la posibilidad de que se produzcan errores.

La próxima vez que esté a punto de escribir construcciones complejas, deténgase un momento y piense cuánto le costará usarlos y si está dispuesto a pagar ese precio.

Escrito por Andrey Karpov.

Este error se encontró con PVS-Studio herramienta de análisis estático.