C++26: loops infinitos triviais não são mais comportamento indefinido

C++26 altera a forma como a linguagem trata loops infinitos “triviais” como `while (true);`, que antes eram comportamento indefinido e podiam ser otimizados de formas surpreendentes, chegando por vezes a fazer o controlo de execução cair em código não relacionado. Os comentadores discutem se esta regra de longa data alguma vez fez sentido, apontando casos reais de uso em código embedded e de kernel onde estes loops são usados para parar em caso de erro ou esperar por interrupções. A escolha do novo padrão de modelar estes loops como chamadas a `std::this_thread::yield()` é vista por alguns como uma forma pragmática de satisfazer os requisitos de “forward progress” e por outros como uma fonte desnecessária de syscalls ocultas e complexidade numa linguagem de programação de sistemas já sobrecarregada por comportamento indefinido.

Escopo da mudança e comportamento surpreendente

  • Muitos comentadores estão chocados com o facto de os loops infinitos triviais serem UB durante “quase 1/6 de um século”, causando coisas como:
    • Funções a “cair” para dentro de funções subsequentes quando o compilador remove o loop e o epílogo.
    • Funções não-void sem return simplesmente a continuar para outro código.
  • Alguns consideram isto uma grande violação das expectativas intuitivas de depuração (“remover código até o problema desaparecer” falha se um loop mínimo pode fazer qualquer coisa).

Porque é que loops infinitos eram UB (otimização / forward progress)

  • Várias publicações ligam isto a “garantias de forward progress”: os compiladores podem assumir que as threads eventualmente fazem progresso (return, I/O, volatile, atomics).
  • UB em loops infinitos permitia:
    • Fusão ou reordenação de loops sem provar terminação.
    • Transformações como elevar stores acima de computações puras.
  • Alguns não estão convencidos de que estas otimizações valham a estranheza semântica, chamando-lhe mais um concurso de benchmarks do que um benefício real para o utilizador.

Casos de uso embarcados e de sistemas

  • Forte desacordo sobre “nunca há uma boa razão” para loops infinitos triviais.
  • Programadores de embedded e bare-metal citam:
    • Handlers de parar-em-erro.
    • “Super loops” do main em sistemas orientados por interrupções.
    • Manter-se acordado em vez de transições dispendiosas entre sleep/wake.
  • Outros argumentam que se devem usar instruções dedicadas de halt/sleep da CPU ou loops que indiquem erro (por exemplo, códigos de piscar LEDs), e não spins vazios.

Novo comportamento em C++26 (std::this_thread::yield)

  • A nova regra define um caso muito estreito: um loop infinitamente vazio e trivial com condição constante.
  • As críticas focam-se em:
    • Substituí-lo por uma chamada implícita a this_thread::yield(), ou seja, uma syscall invisível em código que parece não ter efeitos laterais.
    • Risco em ambientes freestanding / embedded / sandboxed ou em contextos de tempo real.
    • Inconsistência: while(true); vs while(true) continue; vs outras formas terem semânticas diferentes.
  • Alguns preferem isto a UB, mas ainda assim chamam-lhe “horrível” e prefeririam ver:
    • Um diagnóstico/warning.
    • Um primitivo da standard library do tipo “halt aqui”.
    • Ou a regra ao estilo C11 (loops infinitos com condição constante são simplesmente definidos como não terminando).

Preocupações mais amplas sobre UB e a direção do C++

  • Longo subthread sobre UB como ferramenta para otimização versus fonte de miscompilações e comportamento incompreensível.
  • Comparações com C e Rust:
    • O tratamento especial de loops em expressão constante em C é visto como mais sensato.
    • loop {} em Rust e a história do LLVM são mencionados como pressão anterior para corrigir isto.
  • Vários comentários lamentam o comportamento oculto e a complexidade crescentes no C++ moderno, e expressam receio de que o uso freestanding / embedded continue a ficar mais difícil.