En el proceso de pasar al tan esperado título de
Over Senior Engineer principal de C ++ , el año pasado decidí reescribir el juego que desarrollo durante las horas de trabajo (Candy Crush Saga), utilizando la quintaesencia del C ++ moderno (C ++ 17). Y así nació
Meta Crush Saga : un
juego que se ejecuta en la etapa de compilación . Me inspiró mucho el juego
Nibbler de Matt Birner, que utilizó metaprogramación pura en plantillas para recrear la famosa Snake con el Nokia 3310.
“¿Qué tipo de
juego se está ejecutando en la etapa de compilación ?”, “¿Cómo se ve?”, “¿Qué funcionalidad de
C ++ 17 usaste en este proyecto?”, “¿Qué aprendiste?” - Preguntas similares pueden venir a su mente. Para responderlas, tendrás que leer la publicación completa o soportar tu pereza interna y ver una versión en video de la publicación: mi informe del
evento Meetup en Estocolmo:
Nota: en aras de su salud mental y debido a la
errare humanum est , en este artículo se ofrecen algunos datos alternativos.
¿Un juego que se ejecuta en tiempo de compilación?
Creo que para comprender lo que quiero decir con el "concepto" de un
juego ejecutado en la etapa de compilación , es necesario comparar el ciclo de vida de un juego con el ciclo de vida de un juego ordinario.
El ciclo de vida de un juego normal:
Como desarrollador habitual de juegos con una vida normal, trabajando en un trabajo normal con un nivel normal de salud mental, generalmente comienza escribiendo la
lógica del juego en su lenguaje favorito (¡en C ++, por supuesto!), Y luego ejecuta el
compilador para convertir esto, con demasiada frecuencia como espagueti lógica en un
archivo ejecutable . Después de hacer doble clic en el
archivo ejecutable (o comenzar desde la consola), el sistema operativo genera un
proceso . Este
proceso ejecutará la
lógica del
juego , que consiste en un
ciclo de juego en el 99.42% del tiempo.
El ciclo del juego actualiza el estado del juego de acuerdo con ciertas reglas y
la entrada del usuario ,
representa el nuevo estado calculado del juego en píxeles, una y otra vez, y otra vez.
El ciclo de vida de un juego que se ejecuta durante el proceso de compilación:
Como sobre-ingeniero que crea su nuevo y genial juego de compilación, todavía usas tu lenguaje favorito (¡todavía C ++, por supuesto!) Para escribir la
lógica del juego . Luego, como antes,
la fase de compilación continúa, pero hay un giro en la trama:
ejecutas la
lógica del juego en la etapa de compilación. Puede llamarlo "ejecución" (compilación). Y aquí C ++ es muy útil; Tiene características como
Template Meta Programming (TMP) y
constexpr que le permiten realizar
cálculos en la
fase de compilación . Más adelante consideraremos la funcionalidad que se puede usar para esto. Como en esta etapa ejecutamos la
lógica del juego, en este momento también necesitamos insertar la
entrada del jugador . Obviamente, nuestro compilador seguirá creando un
archivo ejecutable en la salida. ¿Para qué se puede usar? El archivo ejecutable ya no contendrá
el ciclo del juego , pero tiene una misión muy simple: mostrar un nuevo
estado calculado . Llamemos a este
archivo ejecutable el procesador y se
procesan los datos que representa . En nuestra
representación, no se contendrán ni los hermosos efectos de partículas ni las sombras de oclusión ambiental, serán ASCII. La
representación ASCII
del nuevo
estado calculado es una propiedad conveniente que se puede demostrar fácilmente al jugador, pero además, la copiamos a un archivo de texto. ¿Por qué un archivo de texto? Obviamente, porque de alguna manera se puede combinar con el
código y volver a realizar todos los pasos anteriores, obteniendo así un
bucle .
Como ya puedes entender, el juego
ejecutado durante el proceso de compilación consiste en un
ciclo de juego en el que cada
cuadro del juego es una
etapa de compilación . Cada
etapa de compilación calcula un nuevo
estado del juego, que puede mostrarse al jugador e insertarse en el siguiente
cuadro /
etapa de compilación .
Puedes contemplar este magnífico diagrama tanto como quieras hasta que entiendas lo que acabo de escribir:
Antes de entrar en detalles sobre la implementación de dicho ciclo, estoy seguro de que quieres hacerme la única pregunta ...
"¿Por qué molestarse en hacer esto?"
¿Realmente crees que arruinar mi idilio de metaprogramación C ++ es una pregunta tan fundamental? Sí, por nada en la vida!
- Lo primero y más importante es que el juego ejecutado en la etapa de compilación tendrá una increíble velocidad de tiempo de ejecución, porque la mayor parte de los cálculos se realizan en la fase de compilación . ¡La velocidad de ejecución es la clave del éxito de nuestro juego AAA con gráficos ASCII!
- Reduce la probabilidad de que aparezcan crustáceos en su repositorio y le pide que vuelva a escribir el juego en Rust . Su discurso bien preparado se desmoronará tan pronto como le explique que no puede existir un puntero inválido en el momento de la compilación. Los programadores seguros de sí mismos de Haskell pueden incluso confirmar la seguridad de tipo en su código.
- Ganará el respeto del reino hipster de Javascript , en el que cualquier marco rediseñado con un síndrome NIH fuerte puede gobernar, siempre que tenga un nombre genial.
- Un amigo mío solía decir que cualquier línea de código Perl puede usarse de facto como una contraseña muy segura. Estoy seguro de que nunca intentó generar contraseñas a partir del tiempo de compilación de C ++ .
Como? ¿Estás satisfecho con mis respuestas? Entonces, tal vez tu pregunta debería ser: "¿Cómo logras hacer esto?"
En realidad, realmente quería experimentar con la funcionalidad agregada en
C ++ 17 . Se pretende que algunas características aumenten la efectividad del lenguaje, así como para la metaprogramación (principalmente constexpr). Pensé que en lugar de escribir ejemplos de código pequeño sería mucho más interesante convertir todo esto en un juego. Los proyectos de mascotas son una excelente manera de aprender conceptos que a menudo no tienes que usar en tu trabajo. La capacidad de ejecutar la lógica básica del juego en tiempo de compilación demuestra nuevamente que las plantillas y constepxr son subconjuntos
completos de Turing del lenguaje C ++.
Reseña del juego Meta Crush Saga
Juego de combinar 3:
Meta Crush Saga es un
juego de unión de fichas similar a
Bejeweled y
Candy Crush Saga . El núcleo de las reglas del juego es conectar tres fichas con el mismo patrón para obtener puntos. Aquí hay un vistazo rápido al
estado del juego que "abandoné" (descargar ASCII es bastante fácil de obtener):
R "(
Saga Meta Crush
------------------------
El | El |
El | RBGBBYGR |
El | El |
El | El |
El | YYGRBGBR |
El | El |
El | El |
El | RBYRGRYG |
El | El |
El | El |
El | RYBY (R) YGY |
El | El |
El | El |
El | BGYRYGGR |
El | El |
El | El |
El | RYBGYBBG |
El | El |
------------------------
> puntuación: 9009
> movimientos: 27
) "
La jugabilidad de este juego Match-3 en sí no es particularmente interesante, pero ¿qué pasa con la arquitectura en la que todo funciona? Para que lo entiendas, intentaré explicarte cada parte del ciclo de vida de este juego en
tiempo de compilación en términos de código.
Inyección del estado del juego:
Si eres un apasionado de C ++ amante o pedante, es posible que hayas notado que el volcado de estado del juego anterior comienza con el siguiente patrón:
R "( . De hecho, este es un
literal de cadena C ++ 11 sin formato , lo que significa que no necesito escapar de caracteres especiales, por ejemplo,
traducción cadenas : el literal de cadena sin procesar se almacena en un archivo llamado
current_state.txt .
¿Cómo inyectamos este estado actual del juego en un estado de compilación? ¡Añádalo a las entradas de bucle!
Ya sea que se trate de un archivo
.txt o un archivo
.h , la directiva de
inclusión del preprocesador C funcionará de la misma manera: copia el contenido del archivo en su ubicación. Aquí copio el literal de cadena sin procesar del estado del juego en ascii a una variable llamada
game_state_string .
Tenga en cuenta que el
archivo de encabezado
loop_inputs.hpp también expande la entrada del teclado al paso de compilación / marco actual. A diferencia del estado del juego, el estado del teclado es bastante pequeño y puede obtenerse fácilmente como una definición de preprocesador.
Calcular un nuevo estado en tiempo de compilación:
Ahora que hemos recopilado suficientes datos, podemos calcular el nuevo estado. Finalmente, hemos llegado al punto donde necesitamos escribir el archivo
main.cpp :
Es extraño, pero este código C ++ no parece tan confuso teniendo en cuenta lo que hace. La mayor parte del código se ejecuta en la fase de compilación, sin embargo, sigue los paradigmas tradicionales de programación de procedimientos y OOP. Solo la última línea, la representación, es un obstáculo para realizar cálculos completos en tiempo de compilación. Como veremos a continuación, lanzando un poco de constexpr en los lugares correctos, podemos obtener una metaprogramación bastante elegante en C ++ 17. Me encanta la libertad que C ++ nos da cuando se trata de ejecución mixta en tiempo de ejecución y compilación.
También notará que este código ejecuta solo un cuadro, no hay
bucle de juego . ¡Resolvamos este problema!
Pegamos todo junto:
Si disgusta mis trucos con
C ++ , espero que no le importe ver mis habilidades de
Bash . De hecho, mi
ciclo de juego no es más que un
script bash que se compila constantemente.
De hecho, estaba teniendo problemas para obtener la entrada del teclado desde la consola. Inicialmente, quería ponerme en paralelo con la compilación. Después de muchas pruebas y errores, logré que algo funcionara más o menos con el comando de
read
de
Bash . Nunca me atrevo a luchar contra el mago
Bash en un duelo: ¡este lenguaje es demasiado siniestro!
Entonces, debo admitir que para administrar el ciclo del juego tuve que recurrir a otro idioma. Aunque técnicamente nada me impidió escribir esta parte del código en C ++. Además, esto no niega el hecho de que el 90% de la lógica de mi juego se ejecuta dentro del equipo de compilación de
g ++ , ¡lo cual es bastante sorprendente!
Un pequeño juego para que tus ojos descansen:
Ahora que has experimentado el tormento de explicar la arquitectura del juego, ha llegado el momento de las pinturas llamativas:
Este gif pixelado es un registro de cómo juego
Meta Crush Saga . Como puede ver, el juego funciona sin problemas para ser jugable en tiempo real. Obviamente, no es tan atractiva como para que pueda transmitir su Twitch y convertirme en la nueva Pewdiepie, ¡pero funciona!
Uno de los aspectos divertidos de almacenar el
estado de un juego en un archivo
.txt es la capacidad de hacer trampa o probar casos extremos muy conveniente.
Ahora que le he presentado brevemente la arquitectura, profundizaremos en la funcionalidad de C ++ 17 utilizada en este proyecto. No consideraré la lógica del juego en detalle, porque se refiere exclusivamente a Match-3, sino que hablaré sobre aspectos de C ++ que se pueden aplicar en otros proyectos.
Mis tutoriales sobre C ++ 17:
A diferencia de C ++ 14, que principalmente contenía correcciones menores, el nuevo estándar C ++ 17 nos puede ofrecer mucho. Había esperanzas de que finalmente aparecieran las características tan esperadas (módulos, corutinas, conceptos ...), pero ... en general ... no aparecieron; nos molestó a muchos de nosotros. Pero después de eliminar el luto, encontramos muchos pequeños tesoros inesperados que, sin embargo, cayeron en el estándar.
¡Me atrevo a decir que los niños que aman la metaprogramación están demasiado mimados este año! Los cambios menores y las adiciones al idioma ahora le permiten escribir código que funciona mucho en tiempo de compilación y después, en tiempo de ejecución.
Constepxr en todos los campos:
Como Ben Dean y Jason Turner predijeron en su
informe sobre C ++ 14 , C ++ le permite mejorar rápidamente la compilación de valores en tiempo de compilación con la palabra clave omnipotente
constexpr . Al ubicar esta palabra clave en los lugares correctos, puede decirle al compilador que la expresión es constante y
se puede evaluar directamente en tiempo de compilación. En
C ++ 11, ya podríamos escribir este código:
constexpr int factorial(int n)
Aunque la palabra clave
constexpr es muy poderosa, tiene bastantes restricciones de uso, lo que dificulta escribir código expresivo de esta manera.
C ++ 14 ha reducido en gran medida los requisitos para
constexpr y se ha vuelto mucho más natural de usar. Nuestra función factorial anterior se puede reescribir de la siguiente manera:
constexpr int factorial(int n) { if (n <= 1) { return 1; } return n * factorial(n - 1); }
C ++ 14 eliminó la regla de que una
función constexpr debería consistir en una sola declaración de retorno, lo que nos obligó a usar el
operador ternario como el bloque de construcción principal. ¡Ahora
C ++ 17 trae aún más aplicaciones de palabras clave
constexpr que podemos explorar!
Ramificación en tiempo de compilación:
¿Alguna vez ha estado en una situación en la que necesita obtener un comportamiento diferente según el parámetro de plantilla que esté manipulando? Supongamos que necesitamos una función parametrizada
serialize
, que llamará a
.serialize()
si el objeto lo proporciona, de lo contrario, recurrirá a llamar a
to_string
para ello. Como se explica con más detalle en esta
publicación sobre SFINAE , lo más probable es que tenga que escribir un código extraterrestre:
template <class T> std::enable_if_t<has_serialize_v<T>, std::string> serialize(const T& obj) { return obj.serialize(); } template <class T> std::enable_if_t<!has_serialize_v<T>, std::string> serialize(const T& obj) { return std::to_string(obj); }
Solo en un sueño podrías reescribir este feo
truco del truco de SFINAE a
C ++ 14 en un código tan magnífico:
Desafortunadamente, cuando despertó y comenzó a escribir
código C ++ 14 real, su compilador arrojó un mensaje desagradable acerca de llamar a
serialize(42);
. Explicó que un
obj
tipo
int
no tiene una función miembro
serialize()
. No importa cómo te enfurezca, ¡el compilador tiene razón! Con este código, siempre intentará compilar ambas ramas:
return obj.serialize();
y
return std::to_string(obj);
. Para
int
branch
return obj.serialize();
Puede resultar ser algún tipo de código muerto, porque
has_serialize(obj)
siempre devolverá
false
, pero el compilador aún tendrá que compilarlo.
Como probablemente haya adivinado,
C ++ 17 nos salva de una situación tan desagradable, porque permitió agregar
constexpr después de la declaración if para "forzar" la ramificación en el momento de la compilación y descartar las construcciones no utilizadas:
Obviamente, esta es una gran mejora
sobre el truco de SFINAE que tuvimos que aplicar antes. Después de eso, comenzamos a tener la misma adicción que Ben y Jason: comenzamos a usar
constexpr en todas partes y siempre. Por desgracia, hay otro lugar donde la palabra clave
constexpr encajaría, pero aún no se usa:
parámetros constexpr .
Parámetros Constexpr:
Si tiene cuidado, puede notar un patrón extraño en el ejemplo de código anterior. Estoy hablando de entradas de bucle:
¿Por qué la variable
game_state_string está encapsulada en una constexpr lambda? ¿Por qué no la convierte en una
variable global constexpr ?
Quería pasar esta variable y su contenido a algunas funciones. Por ejemplo, debe pasarlo a mi
parse_board y usarlo en algunas expresiones constantes:
constexpr int parse_board_size(const char* game_state_string); constexpr auto parse_board(const char* game_state_string) { std::array<GemType, parse_board_size(game_state_string)> board{};
Si seguimos este camino, el compilador
gruñón se quejará de que el parámetro
game_state_string no
es una expresión constante. Cuando creo mi matriz de mosaico, necesito calcular directamente su capacidad fija (no podemos usar vectores en tiempo de compilación porque requieren asignación de memoria) y pasarla como un argumento a la plantilla de valor en
std :: array . Por lo tanto, la
expresión parse_board_size (game_state_string) debe ser una expresión constante. Aunque
parse_board_size está explícitamente marcado como
constexpr ,
game_state_string no es ni puede serlo. En este caso, dos reglas interfieren con nosotros:
- ¡Los argumentos de una función constexpr no son constexpr!
- ¡Y no podemos agregar constexpr delante de ellos!
Todo esto se reduce al hecho de que
las funciones constexpr DEBEN ser aplicables para calcular tanto el tiempo de ejecución como el tiempo de compilación. Suponiendo la existencia de
parámetros constexpr , esto no permitirá que se usen en tiempo de ejecución.
Afortunadamente, hay una manera de nivelar este problema. En lugar de aceptar el valor como un parámetro regular de una función, podemos encapsular este valor en un tipo y pasar este tipo como un parámetro de plantilla:
template <class GameStringType> constexpr auto parse_board(GameStringType&&) { std::array<CellType, parse_board_size(GameStringType::value())> board{};
En este ejemplo de código, estoy creando un tipo estructural
GameString que tiene una función miembro estática constexpr
value () que devuelve el literal de cadena que quiero pasar a
parse_board . En
parse_board, obtengo este tipo a través del
parámetro de plantilla
GameStringType , usando las reglas para extraer argumentos de plantilla. Teniendo un
GameStringType , debido al hecho de que
value () es constexpr, simplemente puedo llamar al
valor de función miembro estático
() en el momento adecuado para obtener un literal de cadena incluso en lugares donde se necesitan expresiones constantes.
Logramos encapsular el literal para pasarlo de alguna manera a
parse_board usando constexpr. Sin embargo, es muy molesto necesitar definir un nuevo tipo cada vez que necesita enviar un nuevo literal
parse_board : "... something1 ...", "... something2 ...". Para resolver este problema en
C ++ 11 , podría aplicar algunas direcciones macro e indirectas feas usando unión anónima y lambda. Michael Park explicó bien este tema en
una de sus publicaciones .
En
C ++ 17, la situación es aún mejor. Si enumeramos los requisitos para pasar nuestro literal de cadena, obtenemos lo siguiente:
- Función generada
- Eso es constexpr
- Con un nombre único o anónimo
Estos requisitos deberían darle una pista. ¡Lo que necesitamos es
constexpr lambda ! Y en
C ++ 17, agregaron completamente la capacidad de usar la
palabra clave constexpr para las funciones lambda. Podemos reescribir nuestro código de muestra de la siguiente manera:
template <class LambdaType> constexpr auto parse_board(LambdaType&& get_game_state_string) { std::array<CellType, parse_board_size(get_game_state_string())> board{};
Créame, esto ya parece mucho más conveniente que la piratería anterior en
C ++ 11 usando macros. Descubrí este increíble truco gracias a
Bjorn Fahler , miembro del grupo m ++ de C ++ en el que participo. Lea más sobre este truco en su
blog . También vale la pena considerar que, de hecho, la palabra clave
constexpr es opcional en este caso: todas las
lambdas con la capacidad de convertirse en
constexpr serán por defecto.
Agregar explícitamente
constexpr es una firma que simplifica nuestra solución de problemas.
Ahora debes entender por qué me obligaron a usar una
constexpr lambda para pasar una cadena que representa el estado del juego. Mire esta función lambda y nuevamente tendrá otra pregunta. ¿Qué es este tipo
constexpr_string que también uso para ajustar el literal de stock?
constexpr_string y constexpr_string_view:
Cuando trabaje con cadenas, no debe procesarlas en el estilo C. ¡Debe olvidar todos estos algoritmos molestos que realizan iteraciones sin procesar y verificar que no se completen! La alternativa que ofrece
C ++ son los
algoritmos omnipotentes
std :: string y
STL . Desafortunadamente,
std :: string puede requerir asignación de memoria en el montón (incluso con Small String Optimization) para almacenar su contenido. Una o dos normas anteriores, podríamos usar
constexpr new / delete o podríamos pasar los
asignadores constexpr a
std :: string , pero ahora necesitamos encontrar otra solución.
Mi enfoque era escribir una clase
constexpr_string con una capacidad fija. Esta capacidad se pasa como un parámetro a la plantilla de valor. Aquí hay una breve descripción de mi clase:
template <std::size_t N>
Mi clase
constexpr_string busca imitar la interfaz
std :: string lo más cerca posible (para las operaciones que necesito): podemos solicitar
iteradores del principio y el final , obtener el
tamaño (tamaño) , acceder a los
datos (datos) ,
eliminar (borrar) parte de ellos, obtener subcadena usando
substr y así sucesivamente. Esto
hace que
sea muy fácil convertir un fragmento de código de
std :: string a
constexpr_string . Quizás se pregunte qué sucede cuando necesitamos usar operaciones que generalmente requieren resaltado en
std :: string . En tales casos, me vi obligado a convertirlos en
operaciones inmutables que crean una nueva instancia de
constexpr_string .
Echemos un vistazo a la operación de
agregar :
template <std::size_t N>
No es necesario tener un premio Fields para suponer que si tenemos una cadena de tamaño
N y una cadena de tamaño
M , entonces una cadena de tamaño
N + M será suficiente para almacenar su concatenación. Podemos desperdiciar parte del "repositorio en tiempo de compilación", ya que ambas líneas pueden no usar toda la capacidad, pero este es un precio bastante pequeño por conveniencia. Obviamente, también escribí un duplicado de
std :: string_view , que se llama
constexpr_string_view .
Con estas dos clases, estaba listo para escribir código elegante para analizar mi
estado de juego . Piensa en algo como esto:
constexpr auto game_state = constexpr_string(“...something...”);
Fue bastante fácil recorrer las joyas en el campo de juego. Por cierto, ¿notaste otra característica preciosa de
C ++ 17 en este ejemplo de código?
Si! No tuve que especificar explícitamente la capacidad de
constexpr_string al construirlo. Anteriormente, cuando se usaba una
plantilla de clase , teníamos que indicar explícitamente sus argumentos. Para evitar estos dolores, creamos funciones
make_xxx porque se pueden rastrear los parámetros
de las plantillas de funciones . Vea cómo el
seguimiento de los argumentos de la plantilla de clase cambia nuestras vidas para mejor:
template <int N> struct constexpr_string { constexpr_string(const char(&a)[N]) {}
En algunas situaciones difíciles, deberá ayudar al compilador a calcular correctamente los argumentos. Si encuentra ese problema, estudie los
manuales para los cálculos de argumentos definidos por el usuario .
Comida gratis de STL:
Bueno, siempre podemos reescribir todo por nuestra cuenta. ¿Pero tal vez los miembros del comité han preparado generosamente algo para nosotros en la biblioteca estándar?
Nuevos tipos de ayuda:
En
C ++ 17 ,
std :: variant y
std :: opcional se agregan a los tipos de diccionario estándar, basados en
constexpr . El primero es muy interesante porque nos permite expresar asociaciones de tipo seguro, pero la implementación en la
biblioteca libstdc ++ con
GCC 7.2 tiene problemas al usar expresiones constantes. Por lo tanto, abandoné la idea de agregar
std :: variant a mi código y usar solo
std :: opcional .
Con el tipo T, el tipo std :: opcional nos permite crear un nuevo tipo std :: opcional <T> , que puede contener un valor de tipo T o nada. Esto es bastante similar a los tipos significativos que permiten un valor indefinido en C # . Veamos la función find_in_board , que devuelve la posición del primer elemento en un campo que confirma que el predicado es correcto. Es posible que no haya tal elemento en el campo. Para manejar esta situación, el tipo de posición debe ser opcional: template <class Predicate> constexpr std::optional<std::pair<int, int>> find_in_board(GameBoard&& g, Predicate&& p) { for (auto item : g.items()) { if (p(item)) { return {item.x, item.y}; }
Anteriormente, teníamos que recurrir a la semántica de los punteros , o agregar un "estado vacío" directamente al tipo de posición, o devolver un booleano y tomar el parámetro de salida . ¡Es cierto que eso fue bastante incómodo!Algunos tipos preexistentes también recibieron soporte constexpr : tuple y pair . No explicaré en detalle su uso, porque ya se ha escrito mucho sobre ellos, pero compartiré una de mis decepciones. El comité agregó azúcar sintáctico al estándar para extraer los valores contenidos en una tupla o par . Este nuevo tipo de declaración llamada enlace estructurado, utiliza paréntesis para especificar en qué variables almacenar la tupla dividida o par : std::pair<int, int> foo() { return {42, 1337}; } auto [x, y] = foo();
Muy inteligente! Pero es una pena que los miembros del comité [no pudieron, no quisieron, no encontraron el tiempo, olvidaron] hacerlos amigables con constexpr . Esperaría algo como esto: constexpr auto [x, y] = foo();
Ahora tenemos contenedores complejos y tipos de ayuda, pero ¿cómo los manipulamos convenientemente?Algoritmos
Actualizar un contenedor para procesar constexpr es una tarea bastante monótona. Comparado con esto, portar constexpr a algoritmos no modificables parece bastante simple. Pero es bastante extraño que en C ++ 17 no vimos progreso en esta área, solo aparecerá en C ++ 20 . Por ejemplo, los maravillosos algoritmos std :: find no recibieron firmas constexpr .Pero no tengas miedo! Como explicaron Ben y Jason, puede convertir fácilmente el algoritmo en constexpr simplemente copiando la implementación actual (pero no se olvide de los derechos de autor); La preferencia es buena. Damas y caballeros, les presento a su atenciónconstexpr std :: find : template<class InputIt, class T> constexpr InputIt find(InputIt first, InputIt last, const T& value) // ^ !!! constexpr. { for (; first != last; ++first) { if (*first == value) { return first; } } return last; }
¡Ya puedo escuchar desde los stands los gritos de los fanáticos de la optimización! Sí, solo agregar constexpr delante del código de muestra proporcionado amablemente por cppreference podría no darnos la velocidad ideal en tiempo de ejecución . Pero si tenemos que mejorar este algoritmo, será necesario para la velocidad en tiempo de compilación . Hasta donde sé, cuando se trata de la velocidad de compilación , las soluciones simples son las mejores.Velocidad y errores:
Los desarrolladores de cualquier juego AAA deberían invertir en resolver estos problemas, ¿verdad?Velocidad:
Cuando logré crear una versión a medias de Meta Crush Saga , el trabajo fue más fácil. De hecho, logré alcanzar un poco más de 3 FPS (cuadros por segundo) en mi vieja computadora portátil con i5 overclockeado a 1.80 GHz (la frecuencia es importante en este caso). Como en cualquier proyecto, rápidamente me di cuenta de que el código escrito anteriormente era asqueroso, y comencé a reescribir el análisis del estado del juego usando constexpr_string y algoritmos estándar. Aunque esto hizo que el código fuera mucho más conveniente de mantener, los cambios afectaron seriamente la velocidad; El nuevo techo es de 0.5 FPS .A pesar del viejo dicho sobre C ++, las "abstracciones de cabeza cero" no son aplicables a los cálculos en tiempo de compilación. Esto es bastante lógico si consideramos al compilador como un intérprete de algún "código de tiempo de compilación". Todavía son posibles mejoras para varios compiladores, pero también hay oportunidades de crecimiento para nosotros, los autores de dicho código. Aquí hay una lista incompleta de observaciones y consejos que encontré, posiblemente específicos para GCC:- Las matrices C funcionan mucho mejor que std :: array . std :: array es un poco de cosméticos modernos de C ++ además de una matriz de estilo C y tiene que pagar un precio por usarlo en tales condiciones.
- , ( ) . , , , . : , , , , ( ) , .
- , . , .
- . GCC. , «».
:
Muchas veces mi compilador arrojó terribles errores de compilación, y mi lógica de código sufrió. Pero, ¿cómo encontrar el lugar donde se esconde el error? Sin un depurador y printf, las cosas se vuelven más complicadas. Si su metafórica "barba del programador" aún no se ha puesto de rodillas (tanto la metafórica como la verdadera barba mía aún están lejos de estas expectativas), entonces tal vez no tenga motivación para usar la luz de techo o depurar el compilador.Nuestro primer amigo será static_assert , lo que nos da la oportunidad de verificar el valor booleano del tiempo de compilación. Nuestro segundo amigo será una macro que habilite y deshabilite constexpr siempre que sea posible: #define CONSTEXPR constexpr
Con esta macro, podemos hacer que la lógica funcione en tiempo de ejecución, lo que significa que podemos adjuntarle un depurador.Meta Crush Saga II: lucha por el juego completamente en tiempo de ejecución:
Obviamente, Meta Crush Saga no ganará los premios The Game Awards este año . Tiene un gran potencial, pero el juego no se ejecuta completamente en tiempo de compilación . Esto puede molestar a los jugadores hardcore ... No puedo deshacerme del script bash a menos que alguien agregue entrada de teclado y lógica impura en la fase de compilación (¡y esto es una locura franca!). Pero creo que algún día podré abandonar por completo el archivo ejecutable del renderizador y mostrar el estado del juego en el momento de la compilación :El loco con el alias saarraz extendió GCC para agregar la construcción static_print al lenguaje . Esta construcción debe tomar varias expresiones constantes o literales de cadena y generarlos en la etapa de compilación. Me alegraría si dicha herramienta se agregara al estándar, o al menos extendiera static_assert para que aceptara expresiones constantes.Sin embargo, en C ++ 17, puede haber una forma de lograr este resultado. Los compiladores ya generan dos cosas: ¡ errores y advertencias ! Si de alguna manera podemos gestionar o cambiar las advertencias a nuestras necesidades, ya recibiremos una conclusión digna. Probé varias soluciones, en particularatributo en desuso : template <char... words> struct useless { [[deprecated]] void call() {}
Aunque la salida obviamente está presente y se puede analizar, desafortunadamente, ¡el código no se puede reproducir! Si, por pura coincidencia, eres miembro de una sociedad secreta de programadores de C ++ que puede realizar la producción durante la compilación, ¡estaré encantado de contratarte en mi equipo para crear la Meta Crush Saga II perfecta !Conclusiones:
Terminé vendiéndote mi juego de estafa . Espero que encuentres esta publicación curiosa y aprendas algo nuevo en el proceso de leerla. Si encuentra errores o formas de mejorar el artículo, contácteme.Quiero agradecer al equipo de SwedenCpp por permitirme realizar el informe de mi proyecto en uno de sus eventos. Además, quiero expresar mi profunda gratitud a Alexander Gurdeev , quien me ayudó a mejorar los aspectos significativos de la saga Meta Crush .