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 aiostreamsy 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.
- Algunos afirman que Rust “no permite” bibliotecas dinámicas en general; otros señalan que existe
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_castpara 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.