No processo de mudança para o tão esperado título de
Lead Senior C ++ Over-Engineer , no ano passado, decidi reescrever o jogo que estou desenvolvendo durante o horário de trabalho (Candy Crush Saga), usando a quintessência do C ++ moderno (C ++ 17). E assim nasceu a
Meta Crush Saga : um
jogo que é executado na fase de compilação . Fiquei muito inspirado pelo jogo
Nibbler de Matt Birner, que usou pura metaprogramação em modelos para recriar a famosa Snake com o Nokia 3310.
“Que tipo de
jogo está sendo executado no estágio de compilação ?”, “Como ele se parece?”, “Que funcionalidade do
C ++ 17 você usou neste projeto?”, “O que você aprendeu?” - Perguntas semelhantes podem vir à sua mente. Para respondê-las, você terá que ler a postagem inteira ou aguentar sua preguiça interior e assistir a uma versão em vídeo da publicação - meu relatório do
evento Meetup em Estocolmo:
Nota: para o bem da sua saúde mental e porque
errare humanum est , são apresentados alguns fatos alternativos neste artigo.
Um jogo que roda em tempo de compilação?
Penso que, para entender o que quero dizer com o "conceito" de um
jogo executado na fase de compilação , você precisa comparar o ciclo de vida de um jogo com o ciclo de vida de um jogo comum.
O ciclo de vida de um jogo regular:
Como um desenvolvedor regular de jogos com uma vida normal, trabalhando em um trabalho regular com um nível normal de saúde mental, você geralmente começa escrevendo a
lógica do jogo no seu idioma favorito (em C ++, é claro!) E depois executa o
compilador para convertê-lo, muitas vezes como espaguete lógica em um
arquivo executável . Após clicar duas vezes no
arquivo executável (ou iniciar no console), o sistema operacional gera um
processo . Esse
processo executará a
lógica do
jogo , que consiste em um
ciclo de jogo em 99,42% do tempo.
O ciclo do jogo atualiza o estado do jogo de acordo com certas regras e informações do
usuário ,
renderiza o novo estado calculado do jogo em pixels, novamente, novamente e novamente.
O ciclo de vida de um jogo em execução durante o processo de compilação:
Como um engenheiro em excesso que cria seu novo jogo legal de compilação, você ainda usa sua linguagem favorita (ainda em C ++, é claro!) Para escrever a
lógica do jogo . Então, como antes,
a fase de compilação continua, mas há uma reviravolta na trama: você
executa a
lógica do jogo na fase de compilação. Você pode chamá-lo de "execução" (compilação). E aqui C ++ é muito útil; possui recursos como
Teta (Template Meta Programming) e
constexpr que permitem realizar
cálculos na
fase de compilação . Mais tarde, consideraremos a funcionalidade que pode ser usada para isso. Como nesta fase executamos a
lógica do jogo, nesse momento também precisamos inserir a
entrada do jogador . Obviamente, nosso compilador ainda criará um
arquivo executável na saída. Para que ele pode ser usado? O arquivo executável não conterá mais
o loop do jogo , mas possui uma missão muito simples: exibir um novo
estado calculado . Vamos chamar esse
arquivo executável de renderizador e
os dados renderizados são
renderizados . Em nossa
renderização, nem belos efeitos de partículas nem sombras de oclusão ambiental serão contidas, será ASCII. A
renderização ASCII
do novo
estado calculado é uma propriedade conveniente que pode ser facilmente demonstrada ao player, mas, além disso, a copiamos para um arquivo de texto. Por que um arquivo de texto? Obviamente, porque ele pode de alguma forma ser combinado com o
código e executar novamente todas as etapas anteriores, obtendo assim um
loop .
Como você já pode entender, o jogo
executado durante o processo de compilação consiste em um
ciclo de jogo no qual cada
quadro do jogo é um
estágio de compilação . Cada
estágio da compilação calcula um novo
estado do jogo, que pode ser mostrado ao jogador e inserido no próximo
quadro /
estágio da compilação .
Você pode contemplar esse diagrama magnífico o quanto quiser até entender o que acabei de escrever:
Antes de entrarmos nos detalhes da implementação desse ciclo, tenho certeza de que deseja me fazer a única pergunta ...
"Por que se preocupar em fazer isso?"
Você realmente acha que arruinar meu idílio de metaprogramação em C ++ é uma questão tão fundamental? Sim, por nada na vida!
- A primeira e mais importante é que o jogo executado na fase de compilação terá uma incrível velocidade de tempo de execução, porque a maior parte dos cálculos é realizada na fase de compilação . A velocidade do tempo de execução é a chave para o sucesso do nosso jogo AAA com gráficos ASCII!
- Você reduz a probabilidade de aparecer algum crustáceo em seu repositório e solicita que você reescreva o jogo no Rust . Seu discurso bem preparado desmoronará assim que você lhe explicar que um ponteiro inválido não pode existir no momento da compilação. Os programadores autoconfiantes de Haskell podem até confirmar a segurança do tipo no seu código.
- Você ganhará o respeito do reino moderno do Javascript , no qual qualquer estrutura reprojetada com uma forte síndrome do NIH pode governar, desde que tenha um nome interessante.
- Um amigo meu costumava dizer que qualquer linha de código Perl pode ser usada de fato como uma senha muito forte. Estou certo de que ele nunca tentou gerar senhas no tempo de compilação do C ++ .
Como Você está satisfeito com minhas respostas? Então talvez sua pergunta deva ser: "Como você consegue fazer isso?"
Na verdade, eu realmente queria experimentar a funcionalidade adicionada no
C ++ 17 . Alguns recursos visam aumentar a eficácia da linguagem, bem como a metaprogramação (principalmente constexpr). Eu pensei que, em vez de escrever pequenos exemplos de código, seria muito mais interessante transformar tudo isso em um jogo. Projetos de animais de estimação são uma ótima maneira de aprender conceitos que você não precisa usar frequentemente em seu trabalho. A capacidade de executar lógica de jogo básica em tempo de compilação novamente prova que modelos e constepxr são subconjuntos
completos de Turing da linguagem C ++.
Revisão do jogo Meta Crush Saga
Jogo de combinação 3:
Meta Crush Saga é um
jogo de
combinação de
peças semelhante ao
Bejeweled e
Candy Crush Saga . O núcleo das regras do jogo é conectar três peças com o mesmo padrão para obter pontos. Aqui está uma rápida olhada no
estado do jogo que eu "despejei" (despejar no ASCII é muito fácil de obter):
R "(
Meta crush saga
------------------------
| |
| RBGBBYGR
| |
| |
| YYGRBGBR
| |
| |
| RBYRGRYG
| |
| |
| RYBY (R) YGY
| |
| |
| BGYRYGGR
| |
| |
| RYBGYBBG
| |
------------------------
> pontuação: 9009
> movimentos: 27
) "
A jogabilidade deste jogo Match-3 em si não é particularmente interessante, mas e a arquitetura na qual tudo funciona? Para que você entenda, tentarei explicar todas as partes do ciclo de vida desse jogo em
tempo de compilação em termos de código.
Injeção do estado do jogo:
Se você é um apaixonado ou pedante apaixonado por C ++, pode ter notado que o despejo de estado do jogo anterior começa com o seguinte padrão:
R "( . Na verdade, esse é um
literal de cadeia de caracteres C ++ 11 bruto , o que significa que não preciso escapar de caracteres especiais, por exemplo,
tradução strings : o literal da string bruta é armazenado em um arquivo chamado
current_state.txt .
Como injetamos esse estado atual do jogo em um estado de compilação? Vamos adicioná-lo às entradas de loop!
Seja um arquivo
.txt ou um arquivo
.h , a diretiva de
inclusão do pré-processador C funcionará da mesma maneira: copia o conteúdo do arquivo para seu local. Aqui eu copio a string bruta literal do estado do jogo em ASCII para uma variável chamada
game_state_string .
Observe que o
arquivo de cabeçalho
loop_inputs.hpp também expande a entrada do teclado para a etapa atual do quadro / compilação. Ao contrário do estado do jogo, o estado do teclado é bastante pequeno e pode ser facilmente obtido como uma definição de pré-processador.
Computando um novo estado em tempo de compilação:
Agora que coletamos dados suficientes, podemos calcular o novo estado. Finalmente, chegamos ao ponto em que precisamos escrever o arquivo
main.cpp :
Estranho, mas esse código C ++ não parece tão confuso, considerando o que faz. A maior parte do código é executada na fase de compilação, no entanto, segue os paradigmas tradicionais de programação processual e POO. Somente a última linha - renderização - é um obstáculo para realizar cálculos completos em tempo de compilação. Como veremos abaixo, lançando um pouco de consexpr nos lugares certos, podemos obter uma metaprogramação bastante elegante no C ++ 17. Acho deliciosa a liberdade que o C ++ nos dá quando se trata de execução mista em tempo de execução e compilação.
Você também notará que esse código executa apenas um quadro, não há
loop de jogo . Vamos resolver esse problema!
Colamos tudo juntos:
Se você repugna meus truques com
C ++ , espero que não se importe em ver minhas habilidades em
Bash . De fato, meu
loop de jogo nada mais é do que um
script bash que é compilado constantemente.
Na verdade, eu estava tendo um pouco de dificuldade em obter a entrada do teclado no console. Inicialmente, eu queria entrar em paralelo com a compilação. Depois de muitas tentativas e erros, consegui trabalhar mais ou menos com o comando
read
do
Bash . Eu nunca ouso lutar contra o bruxo
Bash em um duelo - essa linguagem é muito sinistra!
Portanto, devo admitir que, para gerenciar o ciclo do jogo, tive que recorrer a outro idioma. Embora tecnicamente nada me impedisse de escrever esta parte do código em C ++. Além disso, isso não nega o fato de que 90% da lógica do meu jogo é executada dentro da equipe de compilação do
g ++ , o que é incrível!
Um pouco de jogabilidade para descansar os olhos:
Agora que você experimentou o tormento de explicar a arquitetura do jogo, chegou a hora de pinturas atraentes:
Este gif pixelizado é um registro de como eu jogo
Meta Crush Saga . Como você pode ver, o jogo funciona sem problemas o suficiente para ser jogado em tempo real. Obviamente, ela não é tão atraente que eu possa transmitir seu Twitch e me tornar a nova Pewdiepie, mas ela funciona!
Um dos aspectos divertidos de armazenar o
estado de um jogo em um arquivo
.txt é a capacidade de enganar ou testar casos extremos de maneira muito conveniente.
Agora que apresentei brevemente a arquitetura, vamos nos aprofundar na funcionalidade C ++ 17 usada neste projeto. Não considerarei a lógica do jogo em detalhes, porque se refere exclusivamente ao Match-3; em vez disso, falarei sobre aspectos do C ++ que podem ser aplicados em outros projetos.
Meus tutoriais sobre C ++ 17:
Ao contrário do C ++ 14, que continha principalmente pequenas correções, o novo padrão C ++ 17 pode nos oferecer muito. Havia esperanças de que finalmente os recursos esperados (módulos, corotinas, conceitos ...) finalmente aparecessem, mas ... em geral ... eles não apareciam; isso incomodou muitos de nós. Mas, depois de remover o luto, encontramos muitos pequenos tesouros inesperados que, no entanto, caíram no padrão.
Ouso dizer que as crianças que amam metaprogramação são muito mimadas este ano! Agora, pequenas alterações e adições separadas ao idioma permitem que você escreva um código que funciona muito em tempo de compilação e depois em tempo de execução.
Constepxr em todos os campos:
Como Ben Dean e Jason Turner previram em seu
relatório sobre o C ++ 14 , o C ++ permite melhorar rapidamente a compilação de valores em tempo de compilação com a palavra-chave onipotente
constexpr . Ao localizar essa palavra-chave nos lugares certos, você pode informar ao compilador que a expressão é constante e
pode ser avaliada diretamente no momento da compilação. No
C ++ 11, já poderíamos escrever este código:
constexpr int factorial(int n)
Embora a palavra-chave
constexpr seja muito poderosa, ela possui algumas restrições de uso, dificultando a gravação de código expressivo dessa maneira.
O C ++ 14 reduziu bastante os requisitos para o
constexpr e tornou-se muito mais natural de usar. Nossa função fatorial anterior pode ser reescrita da seguinte forma:
constexpr int factorial(int n) { if (n <= 1) { return 1; } return n * factorial(n - 1); }
O C ++ 14 se livrou da regra de que uma
função constexpr deveria consistir em apenas uma declaração de retorno, o que nos forçou a usar o
operador ternário como o principal componente. Agora, o
C ++ 17 traz ainda mais aplicativos de palavras-chave
constexpr que podemos explorar!
Ramificação em tempo de compilação:
Você já esteve em uma situação em que precisa ter um comportamento diferente, dependendo do parâmetro do modelo que está manipulando? Suponha que precisamos de uma função parametrizada
serialize
, que chamará
.serialize()
se o objeto fornecer, caso contrário, ele recorrerá à chamada
to_string
. Como explicado em mais detalhes neste
post sobre SFINAE , provavelmente você precisará escrever um código alienígena:
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); }
Somente em um sonho você poderia reescrever esse
truque feio do
truque do SFINAE para o
C ++ 14 em um código tão magnífico:
Infelizmente, quando você acordou e começou a escrever
código C ++ 14 real, seu compilador emitiu uma mensagem desagradável sobre a chamada
serialize(42);
. Ele explicou que um
obj
tipo
int
não possui uma função de membro
serialize()
. Não importa como isso o enfurece, o compilador está certo! Com esse código, ele sempre tentará compilar os dois ramos -
return obj.serialize();
e
return std::to_string(obj);
. Para
int
branch
return obj.serialize();
Pode muito bem ser algum tipo de código morto, porque
has_serialize(obj)
sempre retornará
false
, mas o compilador ainda precisará compilá-lo.
Como você provavelmente adivinhou, o
C ++ 17 nos salva de uma situação tão desagradável, porque tornou possível adicionar
constexpr após a instrução if para "forçar" a ramificação no tempo de compilação e descartar construções não utilizadas:
Obviamente, essa é uma grande melhoria em
relação ao truque da SFINAE que tínhamos que aplicar antes. Depois disso, começamos a ter o mesmo vício de Ben e Jason - começamos a usar
constexpr em todos os lugares e sempre. Infelizmente, há outro local onde a palavra-chave
constexpr se ajustaria, mas ainda não foi usada:
parâmetros constexpr .
Parâmetros Constexpr:
Se você for cuidadoso, poderá observar um padrão estranho no exemplo de código anterior. Eu estou falando sobre entradas de loop:
Por que a variável
game_state_string está encapsulada em um lambda constexpr? Por que ela não a torna uma
variável global constexpr ?
Eu queria passar essa variável e seu conteúdo para algumas funções. Por exemplo,
você precisa passá-lo para meu
parse_board e usá-lo em algumas expressões 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{};
Se
seguirmos esse caminho, o compilador ranzinza reclamará que o parâmetro
game_state_string não
é uma expressão constante. Ao criar minha matriz de blocos, preciso calcular diretamente sua capacidade fixa (não podemos usar vetores em tempo de compilação porque eles exigem alocação de memória) e passá-la como argumento para o modelo de valor em
std :: array . Portanto, a
expressão parse_board_size (game_state_string) deve ser uma expressão constante. Embora
parse_board_size esteja explicitamente marcado como
constexpr ,
game_state_string não é e não pode ser! Nesse caso, duas regras interferem conosco:
- Argumentos de uma função constexpr não são constexpr!
- E não podemos adicionar constexpr na frente deles!
Tudo isso se resume ao fato de que
as funções constexpr DEVEM ser aplicáveis ao cálculo do tempo de execução e do tempo de compilação. Assumindo a existência de
parâmetros constexpr , isso não permitirá que eles sejam usados em tempo de execução.
Felizmente, existe uma maneira de nivelar esse problema. Em vez de aceitar o valor como parâmetro regular de uma função, podemos encapsular esse valor em um tipo e passar esse tipo como parâmetro de modelo:
template <class GameStringType> constexpr auto parse_board(GameStringType&&) { std::array<CellType, parse_board_size(GameStringType::value())> board{};
Neste exemplo de código, estou criando um tipo estrutural de
GameString que possui uma função de membro estática constexpr
value () que retorna a literal de cadeia de caracteres que eu quero passar para
parse_board . Em
parse_board, eu recebo esse tipo através do
parâmetro do modelo
GameStringType , usando as regras para extrair argumentos do modelo. Tendo um
GameStringType , devido ao fato de que
value () é constexpr, posso simplesmente chamar a função de membro estático
value () no momento certo para obter uma literal de string mesmo em locais onde são necessárias expressões constantes.
Conseguimos encapsular o literal para, de alguma forma, passá-lo para
parse_board usando constexpr. No entanto, é muito chato precisar definir um novo tipo toda vez que você precisar enviar um novo literal
parse_board : "... alguma
coisa1 ...", "... alguma
coisa2 ...". Para resolver esse problema no
C ++ 11 , você pode aplicar alguns endereços macro e indiretos feios usando união anônima e lambda. Michael Park explicou bem esse tópico em
uma de suas postagens .
No
C ++ 17, a situação é ainda melhor. Se listarmos os requisitos para passar nossa literal de string, obtemos o seguinte:
- Função gerada
- Isso é constexpr
- Com um nome exclusivo ou anônimo
Esses requisitos devem fornecer uma dica. O que precisamos é
constexpr lambda ! E no
C ++ 17, eles adicionaram completamente a capacidade de usar a
palavra -
chave constexpr para funções lambda. Podemos reescrever nosso código de exemplo da seguinte maneira:
template <class LambdaType> constexpr auto parse_board(LambdaType&& get_game_state_string) { std::array<CellType, parse_board_size(get_game_state_string())> board{};
Acredite, isso já parece muito mais conveniente do que o hacking anterior no
C ++ 11 usando macros. Descobri esse truque incrível graças a
Bjorn Fahler , membro do grupo de mitap do C ++ em que participo. Leia mais sobre esse truque em seu
blog . Também vale a pena considerar que, de fato, a palavra-chave
constexpr é opcional neste caso: todas as
lambdas com a capacidade de se tornar
constexpr serão por padrão. A adição explícita de
constexpr é uma assinatura que simplifica nossa solução de problemas.
Agora você deve entender por que fui forçado a usar um
constexpr lambda para transmitir uma string que representa o estado do jogo. Veja esta função lambda e você terá outra pergunta. O que é esse tipo
constexpr_string que eu também uso para quebrar o literal do estoque?
constexpr_string e constexpr_string_view:
Ao trabalhar com strings, você não deve processá-las no estilo C. Você precisa esquecer todos esses algoritmos irritantes que executam iterações brutas e verificam se a conclusão é zero! A alternativa oferecida pelo
C ++ é o onipotente
std :: string e os
algoritmos STL . Infelizmente,
std :: string pode exigir alocação de memória no heap (mesmo com a Small String Optimization) para armazenar seu conteúdo. Um ou dois padrões anteriores, poderíamos usar
constexpr new / delete ou passar os
alocadores constexpr para
std :: string , mas agora precisamos encontrar outra solução.
Minha abordagem foi escrever uma classe
constexpr_string com uma capacidade fixa. Essa capacidade é passada como um parâmetro para o modelo de valor. Aqui está uma breve visão geral da minha turma:
template <std::size_t N>
Minha classe
constexpr_string procura imitar a interface
std :: string o mais próximo possível (para as operações que preciso): podemos solicitar
iteradores do início e do fim , obter o
tamanho (tamanho) , acessar os
dados (dados) ,
excluir (apagar) parte deles, obter substring usando
substr e assim por diante. Isso facilita a conversão de um pedaço de código de
std :: string para
constexpr_string . Você pode se perguntar o que acontece quando precisamos usar operações que geralmente exigem destaque em
std :: string . Nesses casos, fui forçado a convertê-los em
operações imutáveis que criam uma nova instância de
constexpr_string .
Vamos dar uma olhada na operação de
acréscimo :
template <std::size_t N>
Você não precisa ter um prêmio Fields para assumir que, se tivermos uma sequência de tamanho
N e uma sequência de tamanho
M , uma sequência de tamanho
N + M será suficiente para armazenar sua concatenação. Podemos desperdiçar parte do "repositório em tempo de compilação", pois as duas linhas podem não usar toda a capacidade, mas esse é um preço bastante pequeno por conveniência. Obviamente, também escrevi uma duplicata do
std :: string_view , que chamava
constexpr_string_view .
Com essas duas classes, eu estava pronto para escrever um código elegante para analisar meu
estado de jogo . Pense em algo como isto:
constexpr auto game_state = constexpr_string(“...something...”);
Era muito fácil percorrer as joias no campo de jogo - a propósito, você notou outro recurso precioso do
C ++ 17 neste exemplo de código?
Sim Não tive que especificar explicitamente a capacidade de
constexpr_string ao construí-lo. Anteriormente, ao usar um
modelo de classe , tínhamos que indicar explicitamente seus argumentos. Para evitar essas dores, criamos funções
make_xxx porque os parâmetros
dos modelos de função podem ser rastreados. Veja como
acompanhar os argumentos do modelo de classe muda nossas vidas para melhor:
template <int N> struct constexpr_string { constexpr_string(const char(&a)[N]) {}
Em algumas situações difíceis, você precisará ajudar o compilador a calcular corretamente os argumentos. Se você encontrar esse problema, estude os
manuais para cálculos de argumentos definidos pelo usuário .
Comida grátis da STL:
Bem, sempre podemos reescrever tudo por conta própria. Mas talvez os membros do comitê tenham generosamente preparado algo para nós na biblioteca padrão?
Novos tipos de auxiliar:
No
C ++ 17 ,
std :: variant e
std :: optional são adicionados aos tipos de dicionário padrão, com base no
constexpr . O primeiro é muito interessante porque nos permite expressar associações de tipo seguro, mas a implementação na
biblioteca libstdc ++ com o
GCC 7.2 tem problemas ao usar expressões constantes. Portanto, abandonei a ideia de adicionar
std :: variant ao meu código e use apenas
std :: optional .
Com o tipo T, o tipo std :: optional nos permite criar um novo tipo std :: optional <T> , que pode conter um valor do tipo T ou nada. Isso é bastante semelhante aos tipos significativos que permitem valor indefinido em C # . Vejamos a função find_in_board , que retorna a posição do primeiro elemento em um campo que confirma que o predicado está correto. Pode não haver esse elemento no campo. Para lidar com essa situação, o tipo de posição deve 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, tínhamos que recorrer à semântica dos ponteiros , ou adicionar um "estado vazio" diretamente ao tipo de posição, ou retornar um booleano e pegar o parâmetro de saída . É certo que isso foi bastante estranho!Alguns tipos pré-existentes também receberam suporte constexpr : tupla e par . Não explicarei em detalhes seu uso, porque muito já foi escrito sobre eles, mas compartilharei uma de minhas decepções. O comitê adicionou açúcar sintático ao padrão para extrair os valores contidos em uma tupla ou par . Esse novo tipo de declaração chamado ligação estruturada, usa parênteses para especificar em quais variáveis armazenar a tupla ou par dividido : std::pair<int, int> foo() { return {42, 1337}; } auto [x, y] = foo();
Muito esperto! Mas é uma pena que os membros do comitê [não pudessem, não quisessem, não encontrassem tempo, se esquecessem] de torná-los amigáveis para se expressar . Eu esperaria algo assim: constexpr auto [x, y] = foo();
Agora temos contêineres complexos e tipos auxiliares, mas como os manipulamos convenientemente?Algoritmos:
Atualizar um contêiner para processar constexpr é uma tarefa bastante monótona. Comparado a isso, portar constexpr para algoritmos que não modificam parece bastante simples. Mas é bastante estranho que no C ++ 17 não vimos progresso nessa área, ele aparecerá apenas no C ++ 20 . Por exemplo, os maravilhosos algoritmos std :: find não receberam assinaturas constexpr .Mas não tenha medo! Como Ben e Jason explicaram, você pode facilmente transformar o algoritmo em constexpr simplesmente copiando a implementação atual (mas não se esqueça dos direitos autorais); cppreference é bom. Senhoras e senhores, apresento a sua atençãoconstexpr 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; }
Já posso ouvir dos estandes os gritos dos fãs de otimização! Sim, apenas adicionar constexpr na frente do código de amostra gentilmente fornecido pelo cppreference pode não nos dar a velocidade ideal em tempo de execução . Mas se precisarmos melhorar esse algoritmo, será necessário obter velocidade no tempo de compilação . Tanto quanto eu sei, quando se trata de velocidade de compilação , soluções simples são as melhores.Velocidade e bugs:
Os desenvolvedores de qualquer jogo AAA devem investir na solução desses problemas, certo?Velocidade:
Quando consegui criar uma versão semi-funcional do Meta Crush Saga , o trabalho foi mais suave. Na verdade, consegui obter um pouco mais de 3 FPS (quadros por segundo) no meu laptop antigo com o i5 com overclock para 1,80 GHz (a frequência é importante neste caso). Como em qualquer projeto, percebi rapidamente que o código escrito anteriormente era nojento e comecei a reescrever a análise do estado do jogo usando constexpr_string e algoritmos padrão. Embora isso tenha tornado o código muito mais conveniente de manter, as alterações afetaram seriamente a velocidade; o novo teto é de 0,5 FPS .Apesar do velho ditado sobre C ++, "abstrações sem cabeçalho" não são aplicáveis a cálculos em tempo de compilação. Isso é bastante lógico se considerarmos o compilador como um intérprete de algum "código de tempo de compilação". Melhorias para vários compiladores ainda são possíveis, mas também existem oportunidades de crescimento para nós, os autores desse código. Aqui está uma lista incompleta de observações e dicas que encontrei, possivelmente específicas do GCC:- Matrizes C funcionam muito melhor do que std :: array . O std :: array é um pouco de cosméticos C ++ modernos em cima de um array no estilo C e você precisa pagar um preço por usá-lo nessas condições.
- , ( ) . , , , . : , , , , ( ) , .
- , . , .
- . GCC. , «».
:
Muitas vezes, meu compilador gerou terríveis erros de compilação e minha lógica de código sofreu. Mas como encontrar o lugar onde o bug está escondido? Sem depurador e printf, as coisas ficam mais complicadas. Se sua "barba metafórica do programador" ainda não se ajoelhou (tanto a barba metafórica quanto a barba real ainda estão longe dessas expectativas), então talvez você não tenha motivação para usar a luz direta ou depurar o compilador.Nosso primeiro amigo será static_assert , o que nos dá a oportunidade de verificar o valor booleano do tempo de compilação. Nosso segundo amigo será uma macro que habilita e desabilita o constexpr sempre que possível: #define CONSTEXPR constexpr
Com essa macro, podemos fazer a lógica funcionar em tempo de execução, o que significa que podemos anexar um depurador a ela.Meta Crush Saga II - lute pela jogabilidade completamente em tempo de execução:
Obviamente, a Meta Crush Saga não ganhará o The Game Awards este ano . Tem um grande potencial, mas a jogabilidade não é totalmente executada em tempo de compilação . Isso pode incomodar os jogadores hardcore ... Não consigo me livrar do script bash, a menos que alguém adicione entrada do teclado e lógica suja na fase de compilação (e isso é loucura!). Mas acredito que um dia poderei abandonar completamente o arquivo executável do renderizador e exibir o estado do jogo em tempo de compilação :O maluco com o pseudônimo saarraz estendeu o GCC para adicionar a construção static_print ao idioma . Essa construção deve pegar várias expressões constantes ou literais de string e produzi-las no estágio de compilação. Eu ficaria feliz se uma ferramenta desse tipo fosse adicionada ao padrão ou, pelo menos, estendida static_assert para que ela aceite expressões constantes.No entanto, no C ++ 17, pode haver uma maneira de alcançar esse resultado. Os compiladores já produzem duas coisas - erros e avisos ! Se, de alguma maneira, pudermos gerenciar ou alterar os avisos para nossas necessidades, já receberemos uma conclusão digna. Eu tentei várias soluções, em particularatributo obsoleto : template <char... words> struct useless { [[deprecated]] void call() {}
Embora a saída esteja obviamente presente e possa ser analisada, infelizmente, o código não pode ser reproduzido! Se, por pura coincidência, você for membro de uma sociedade secreta de programadores em C ++ que pode executar resultados durante a compilação, ficarei feliz em contratá-lo em minha equipe para criar a perfeita Meta Crush Saga II !Conclusões:
Acabei vendendo o meu jogo de fraude . Espero que você ache este post curioso e aprenda algo novo no processo de lê-lo. Se você encontrar erros ou maneiras de melhorar o artigo, entre em contato comigo.Quero agradecer à equipe do SwedenCpp por me permitir conduzir meu relatório de projeto em um de seus eventos. Além disso, quero expressar minha profunda gratidão a Alexander Gurdeev , que me ajudou a melhorar os aspectos significativos da Saga Meta Crush .