El arma secreta del depurador: lo que los programadores de C++ pasan por alto
\n\n¿Alguna vez has escrito algo como i = i++ + ++i; y has obtenido resultados diferentes en distintos compiladores? ¿O has detectado un error extraño que solo se reproducía en el servidor pero no en tu portátil? Conoce el comportamiento indefinido — uno de los enemigos más insidiosos de un desarrollador de C++. Y en el centro de este caos están los puntos de secuencia.
En este artículo, desglosaremos qué son los puntos de secuencia, cómo se relacionan con el orden de evaluación en C++, y — lo más importante — cómo evitar pisar estos rastrillos. El material está basado en una pregunta clásica de Stack Overflow que ha acumulado miles de visitas. ¡Vamos allá!
\n\n¿Qué son los puntos de secuencia?
\n\nImagina que eres un director de orquesta. Cada músico (instrucción del procesador) debe entrar en un momento estrictamente definido. Un punto de secuencia es un momento en el tiempo en el que todos los "efectos secundarios" anteriores (cambios de variables, llamadas a funciones) están garantizados de estar completos, y los siguientes aún no han comenzado.
\n\nEn el estándar C++ 98/03, se utilizaba el término punto de secuencia. En C++11, fue reemplazado por las relaciones más estrictas "secuenciado antes / secuenciado después", pero la esencia sigue siendo: entre dos puntos de secuencia, el compilador tiene todo el derecho de reordenar operaciones, siempre que no cambie el resultado (según la regla "as-if").
\n\nEn pocas palabras: si intentas modificar la misma variable dos veces entre dos puntos de secuencia — obtienes comportamiento indefinido. El compilador puede hacer cualquier cosa: desde un resultado correcto hasta formatear tu disco duro (es broma, pero casi cierto).
\n\nEjemplos clásicos de puntos de secuencia
\n- \n
- El punto y coma
;— final de una expresión \n - La coma
,como operador (¡pero no en los argumentos de una función!) \n - El final de cada expresión en un bucle
for(después de la inicialización, condición e incremento) \n - Los operadores lógicos
&&y||\n - El operador ternario
?:\n - Una llamada a función (antes de que el cuerpo de la función comience a ejecutarse) \n
¿Cómo causan los puntos de secuencia comportamiento indefinido?
\n\nAquí hay un ejemplo clásico que todo desarrollador de C++ debería conocer:
\n\nint i = 0;\ni = i++ + ++i; // UB! Comportamiento indefinido\n\n¿Por qué es UB? Porque entre dos puntos de secuencia (en este caso, entre el inicio y el final de la expresión), la variable i es modificada dos veces:
- \n
- El incremento postfijo
i++\n - El incremento prefijo
++i\n - La asignación
i = ...\n
El compilador no sabe en qué orden realizar estas operaciones. ¿El resultado? Un compilador devolverá 2, otro — 3, un tercero — 0. Y en modo de optimización, incluso podría descartar este código por completo.
\n\nOtro caso complicado: argumentos de función
\n\nint foo(int a, int b) { return a + b; }\n\nint x = 5;\nint result = foo(++x, ++x); // UB!\n\nAquí, la coma entre los argumentos no es un operador coma, sino un separador. ¡No hay un punto de secuencia entre la evaluación de los argumentos ++x! Ambos incrementos ocurren "en paralelo" (desde la perspectiva del estándar), y nuevamente obtenemos comportamiento indefinido.
Ejemplos prácticos: cómo se ve el UB en código real
\n\nEjemplo 1: Un error no obvio con indexación
\n\nint arr[5] = {0, 10, 20, 30, 40};\nint idx = 1;\narr[idx] = idx++; // UB!\n\nA primera vista, parece simple: queremos escribir 1 en arr[1], y luego incrementar idx. Pero el estándar no garantiza el orden de evaluación del índice idx y el lado derecho idx++. ¿El resultado? Podría escribir en arr[1], o podría escribir en arr[2].