La lista traviesa y buena de C++20 para desarrolladores de juegos

Las nuevas funciones de C++20 están generando reacciones encontradas entre los desarrolladores de juegos, que agradecen comodidades como el operador de comparación de tres vías, `std::bit_cast` y las coroutines, pero se preocupan por los tiempos de compilación, el bloat de código y las carencias de tooling. El formateo de cadenas con `{fmt}`/`std::format` y los inicializadores designados ilustran el intercambio entre APIs más seguras y expresivas y plantillas más pesadas o reglas de inicialización más estrictas, algo que importa mucho en codebases AAA de 10M+ líneas. Algunos desarrolladores están refactorizando hacia patrones similares a Rust o considerando reescrituras completas en Rust, argumentando que la seguridad y la ergonomía pueden superar las pequeñas mejoras de rendimiento ligadas a los UB de C++ y a su margen de optimización de bajo nivel.

Operador de comparación de tres vías <=>

  • Varios comentarios explican que definir <=> para un tipo genera automáticamente los seis operadores de comparación (<, >, <=, >=, ==, !=), reduciendo el código repetitivo.
  • Simplifica la implementación para objetos similares, ya que solo una rutina necesita ser “conectada”.

std::format / {fmt}: rendimiento, bloat y tiempos de compilación

  • A algunos les sorprende ver que se critique fmt/format.h; otros coinciden en que las plantillas pesadas y el estilo header-only perjudican los tiempos de compilación en grandes codebases de C++20.
  • Una parte sostiene que el bloat de tamaño de código se mitiga en gran medida con los linkers modernos y moviendo la lógica pesada a archivos .cc; la preocupación mayor es el tiempo de compilación.
  • Quienes defienden {fmt} dicen que está optimizado para la velocidad de compilación, puede superar a iostreams y se usa ampliamente en formato numérico e I/O de rendimiento crítico.
  • Hay confusión sobre por qué el patrón sugerido de “despachar a una TU no plantilla” no podría usarse también con <format>.

Funciones de C++20 en grandes codebases de juegos

  • La discusión se centra en que 10M+ LOC es realista para motores AAA (con ejemplos como Unreal), y en el dolor de las compilaciones incrementales y, especialmente, de los tiempos de enlace.
  • Las compilaciones distribuidas y herramientas como Incredibuild ayudan, pero los linkers (en particular el de MSVC) siguen siendo un cuello de botella.
  • Algunos cuestionan por qué alguien recompila a menudo más de unos pocos archivos; otros señalan que los cambios en cabeceras núcleo o flags del compilador disparan reconstrucciones enormes.

Inicializadores designados y semántica de C vs C++

  • A muchos les disgusta que los inicializadores designados en C++ deban seguir el orden de declaración, a diferencia de C.
  • Las justificaciones incluyen:
    • El orden de inicialización y destrucción de miembros importa en C++.
    • Los miembros pueden depender de la inicialización de miembros anteriores (p. ej., int b = a + 1).
    • Permitir un orden arbitrario crearía errores sutiles o requeriría romper reglas existentes.
  • Algunos argumentan que las structs POD “al estilo C” podrían relajarse, pero otros señalan que eso sería frágil cuando cambien las structs.
  • Varios consideran que el inicializado designado restringido de C++ sigue siendo útil, aunque menos potente que el de C99 (sin cadenas anidadas ni designadores de índice de array).

Rust vs C++ para motores de juego y carga dinámica

  • Un desarrollador está tentado de reescribir un motor de juego en C++ en Rust por ergonomía, pero teme el impacto en el calendario, así que está haciendo C++ “listo para Rust” (propiedad, diseño orientado a datos).
  • Hay debate sobre Rust y bibliotecas dinámicas:
    • Algunos afirman que Rust “no permite” bibliotecas dinámicas en general; otros señalan que existe dylib, pero a menudo requiere ABI de C para la interoperabilidad.
    • Se citan como gran ventaja los flujos de trabajo de juegos como la recarga en caliente de DLL con “C++ normal”; en la práctica, tanto C++ como Rust suelen recurrir a ABIs de C para límites estables de plugins.
    • Se discute un modelo alternativo de plugins mediante procesos separados e IPC, pero se considera órdenes de magnitud más lento que las llamadas en proceso.

UB por overflow con signo y optimización

  • Un comentarista sostiene que el beneficio de rendimiento de mantener indefinido el overflow con signo es pequeño (por ejemplo, a veces evitar una instrucción de extensión de signo), y solo afecta a patrones concretos de bucles.
  • Otros piden aclaraciones; las explicaciones se centran en cómo los compiladores razonan sobre índices de bucle e indexación de arrays bajo la suposición de que no hay overflow.
  • Hay apoyo para definir el overflow con signo como wrap (como -fwrapv) o para garantizar trampas de overflow; el UB actual se ve como un mal intercambio por una ganancia de velocidad diminuta.

std::bit_cast, unions y evaluación en tiempo de compilación

  • A algunos les alivia tener std::bit_cast para reemplazar el type punning basado en unions, que es UB en C++ aunque los compiladores normalmente lo “acepten” en la práctica.
  • Esto ayuda a los revisores a impulsar patrones más seguros, especialmente para estudiantes que vienen de C.
  • Se menciona std::is_constant_evaluated(), pero no se explora realmente; sus beneficios para cargas de trabajo numéricas/físicas pesadas siguen sin quedar claros en el hilo.

Coroutines y tooling / depuración

  • Para algunos, las coroutines están en la lista de “lo bueno”: pueden ser más rápidas y seguras en tipos que los callbacks, y a menudo hacen más legible el flujo async.
  • Un gran punto doloroso es la depuración: los stack traces de fallos en coroutines a menudo muestran solo frames del framework/boilerplate, no código de usuario.
  • Algunos señalan que depurar código lleno de callbacks también es doloroso, y aun así prefieren coroutines pese a las carencias de tooling.

Ranges, lambdas y flujo de control que “no linealiza”

  • Una perspectiva es que el uso intensivo de ranges y lambdas inline hace que el código sea más difícil de seguir:
    • El código fuera de las lambdas se ejecuta primero, mientras que los cuerpos de las lambdas pueden ejecutarse más tarde, varias veces o nunca.
    • Las capturas por referencia/valor pueden introducir errores sutiles de vida útil y mutación si no se razona con cuidado.
  • Otros reconocen esto, pero aun así consideran que las coroutines y abstracciones de nivel superior valen la pena frente a callbacks profundamente anidados.