BookinglyTech News
Software

C++26 deja de tratar los bucles infinitos triviales como comportamiento indefinido

El paper P2809R3 define los bucles infinitos triviales y obliga a sustituir su cuerpo por una llamada a std::this_thread::yield(). Se aceptó también como defect report, así que puede llegar a modos anteriores.

3 min de lecturaLobsters0 vistas

Un while (true); sin cuerpo ni efectos secundarios era comportamiento indefinido en C++. Hasta ahora. El comité de C++ ha aprobado el paper P2809R3, que define esta construcción y cierra una divergencia con C que llevaba quince años abierta. El cambio entró además como defect report, así que las implementaciones pueden aplicarlo también a modos anteriores: es posible que en un compilador reciente ya no se reproduzca el comportamiento antiguo ni siquiera con -std=c++20.

El caso clásico es este:

int main() {
    while (true);
}

Antes de C++26, el optimizador podía asumir que ese bucle termina y borrarlo. Clang lo hacía, y el resultado es el que cabría esperar de un programa al que le quitan el bucle principal: la ejecución cae a lo que el enlazador haya colocado justo después. En el ejemplo en godbolt, con una función unreachable() detrás de main, el binario imprime "Hello world!". No es un bug del compilador; es exactamente lo que permite la norma.

De dónde viene el agujero

El origen está en la garantía de progreso (forward progress) que C++11 introdujo junto con el soporte de hilos. La norma permite suponer que cualquier hilo acabará haciendo una de estas cosas: terminar, llamar a una función de E/S de la biblioteca, acceder a un glvalue volátil o ejecutar una operación atómica o de sincronización. Un while (true); no hace ninguna. Por tanto, una ejecución que se queda ahí indefinidamente es comportamiento indefinido, y el optimizador puede tratarla como código muerto.

C11 introdujo las mismas reglas, pero añadió una excepción: los bucles cuya expresión de control es una constante no pueden darse por terminados. C++ nunca copió esa cláusula. El resultado es que while (1); estaba bien definido en C y era UB en C++, algo que rompía de verdad.

El patrón aparece sobre todo en código embebido y en kernels: cuando falla la inicialización del hardware y no hay sistema operativo al que salir, el programa se queda ahí parado. Con el bucle eliminado, el manejador de error fatal no detiene nada: la máquina sigue ejecutando instrucciones en un estado corrupto. En código con requisitos de seguridad, eso es una vulnerabilidad real.

Qué cambia exactamente

El comité no adoptó la regla de C, que protege demasiados bucles e inhibe optimizaciones útiles. P2809R3 acota mucho más y habla de trivial infinite loop, con dos condiciones: el cuerpo está literalmente vacío (; o {}) y la expresión de control es una constante que evalúa a true. Cuando se cumplen, el cuerpo se sustituye por una llamada a std::this_thread::yield(), que le da al bucle la semántica de progreso que le faltaba. Encajan while (true);, for (;;);, do {} while (true); y también while (go); si go es constexpr. No encajan un cuerpo con cualquier sentencia, aunque sea inútil, ni un while (true) if (done) break;, ni usar una variable no constante como condición.

Queda un matiz para bare metal: en implementaciones freestanding es decisión de la implementación si se aplica o no el reemplazo por yield. Tiene sentido, porque convertir un parón deliberado en una cesión cooperativa cambia el comportamiento que el programador escribió.

La consecuencia práctica es que el optimizador ya no puede tratar estos bucles como UB ni asumir que la ejecución continúa después. Si mantienes código embebido con patrones de halt-on-error, merece la pena comprobar qué hace tu cadena de compilación actual.