Provavelmente, no título do artigo, você já adivinhou que o foco está nos erros no código-fonte. Mas essa não é a única coisa que será discutida neste artigo. Se, além de C ++ e erros no código de outra pessoa, você é atraído por jogos incomuns e está interessado em saber o que são esses "bagels" e o que eles comem com eles, bem-vindo ao kat!
Na minha busca por jogos incomuns, me deparei com o jogo Cataclysm Dark Days Ahead, que difere de outros gráficos incomuns: ele é implementado usando caracteres ASCII multicoloridos em um fundo preto.
O que é impressionante neste jogo e seu tipo é o quanto tudo é implementado neles. Especificamente, no Cataclysm, por exemplo, mesmo para criar um personagem, quero procurar guias, pois existem dezenas de parâmetros, recursos e gráficos iniciais diferentes, sem mencionar as variações de eventos no próprio jogo.
Este é um jogo de código aberto e também escrito em C ++. Portanto, era impossível passar e não executar este projeto através do analisador estático PVS-Studio, no qual estou envolvido ativamente no desenvolvimento. O projeto em si me surpreendeu com a alta qualidade do código, no entanto, ele ainda contém algumas falhas e discutirei várias delas neste artigo.
Até o momento, muitos jogos foram testados usando o PVS-Studio. Por exemplo, você pode ler nosso outro artigo, “
Análise estática no setor de videogames: os 10 principais erros de software ”.
Lógica
Exemplo 1:O exemplo a seguir é um erro de cópia típico.
V501 Existem
subexpressões idênticas à esquerda e à direita do '||' operador: rng (2, 7) <abs (z) || rng (2, 7) <abs (z) overmap.cpp 1503
bool overmap::generate_sub( const int z ) { .... if( rng( 2, 7 ) < abs( z ) || rng( 2, 7 ) < abs( z ) ) { .... } .... }
Aqui a mesma condição é verificada duas vezes. Provavelmente, a expressão foi copiada e esqueceu de mudar algo nela. Acho difícil dizer se esse erro é significativo, mas a verificação não funciona conforme o esperado.
Um aviso semelhante:
- V501 Existem subexpressões idênticas 'one_in (100000 / to_turns <int> (dur))' à esquerda e à direita do operador '&&'. player_hardcoded_effects.cpp 547
Exemplo 2:V728 Uma verificação excessiva pode ser simplificada. O '(A && B) || (! A &&! B) 'expressão é equivalente à expressão' bool (A) == bool (B) '. inventário_ui.cpp 199
bool inventory_selector_preset::sort_compare( .... ) const { .... const bool left_fav = g->u.inv.assigned.count( lhs.location->invlet ); const bool right_fav = g->u.inv.assigned.count( rhs.location->invlet ); if( ( left_fav && right_fav ) || ( !left_fav && !right_fav ) ) { return .... } .... }
Não há erro na condição, mas é desnecessariamente complicado. Vale a pena ter pena daqueles que têm que desmontar essa condição, e é mais fácil escrever
if (left_fav == right_fav) .
Um aviso semelhante:
- V728 Uma verificação excessiva pode ser simplificada. O '(A &&! B) || (! A && B) 'expressão é equivalente à expressão' bool (A)! = Bool (B) '. iuse_actor.cpp 2653
Retiro I
Acabou sendo uma descoberta para mim que os jogos que hoje são chamados de "bagels" são apenas seguidores bastante leves do velho gênero de jogos roguelike. Tudo começou com o culto jogo Rogue de 1980, que se tornou um modelo e inspirou muitos estudantes e programadores a criar seus próprios jogos. Eu acho que muita coisa também foi trazida pela comunidade de role-playing do DnD e suas variações.
Microoptimização
Exemplo 3:O próximo grupo de avisos do analisador não indica um erro, mas a possibilidade de micro otimização do código do programa.
V801 desempenho reduzido. É melhor redefinir o argumento da segunda função como referência. Considere substituir 'const ... type' por 'const ... & type'. map.cpp 4644
template <typename Stack> std::list<item> use_amount_stack( Stack stack, const itype_id type ) { std::list<item> ret; for( auto a = stack.begin(); a != stack.end() && quantity > 0; ) { if( a->use_amount( type, ret ) ) { a = stack.erase( a ); } else { ++a; } } return ret; }
Aqui o
itdpe_id oculta
std :: string . Como o argumento ainda é passado constante, o que não permitirá que ele seja alterado, seria mais rápido passar apenas uma referência variável à função e não desperdiçar recursos na cópia. E embora, muito provavelmente, a linha seja muito pequena, mas cópias constantes sem motivo aparente sejam desnecessárias. Além disso, essa função é chamada de lugares diferentes, muitos dos quais, por sua vez, também obtêm o
tipo de fora e o copiam.
Avisos semelhantes:
- V801 desempenho reduzido. É melhor redefinir o argumento da terceira função como referência. Considere substituir 'const ... evt_filter' por 'const ... & evt_filter'. input.cpp 691
- V801 desempenho reduzido. É melhor redefinir o argumento da quinta função como referência. Considere substituir 'const ... color' por 'const ... & color'. output.h 207
- No total, o analisador gerou 32 desses avisos.
Exemplo 4:V813 Desempenho Diminuído. O argumento 'str' provavelmente deve ser renderizado como uma referência constante. catacharset.cpp 256
std::string base64_encode( std::string str ) { if( str.length() > 0 && str[0] == '#' ) { return str; } int input_length = str.length(); std::string encoded_data( output_length, '\0' ); .... for( int i = 0, j = 0; i < input_length; ) { .... } for( int i = 0; i < mod_table[input_length % 3]; i++ ) { encoded_data[output_length - 1 - i] = '='; } return "#" + encoded_data; }
Nesse caso, o argumento, embora não seja constante, não muda no corpo da função. Portanto, para otimização, seria bom passá-lo por um link constante e não forçar o compilador a criar cópias locais.
Esse aviso também não era único; havia 26 casos no total.
Avisos semelhantes:
- V813 Desempenho Diminuído. O argumento 'message' provavelmente deve ser renderizado como uma referência constante. json.cpp 1452
- V813 Desempenho Diminuído. O argumento 's' provavelmente deve ser renderizado como uma referência constante. catacharset.cpp 218
- E assim por diante ...
Retiro II
Alguns dos clássicos jogos roguelike ainda estão sendo desenvolvidos. Se você for aos repositórios GitHub Cataclysm DDA ou NetHack, poderá ver que as mudanças estão sendo feitas ativamente todos os dias. O NetHack é geralmente o jogo mais antigo que ainda está sendo desenvolvido: foi lançado em julho de 1987 e a versão mais recente data de 2018.
No entanto, um dos famosos jogos posteriores desse gênero é o Dwarf Fortress, desenvolvido desde 2002 e lançado pela primeira vez em 2006. "Perder é divertido" é o lema do jogo, que reflete com precisão sua essência, pois é impossível vencê-lo. Este jogo em 2007 ganhou o título de melhor jogo roguelike do ano como resultado da votação, realizada anualmente no site ASCII GAMES.
By the way, aqueles que estão interessados neste jogo podem estar interessados nas seguintes notícias. O Dwarf Fortress será lançado no Steam com gráficos aprimorados de 32 bits. Com uma imagem atualizada em que dois moderadores experientes estão trabalhando, a versão premium do Dwarf Fortress receberá faixas de música adicionais e suporte para o Steam Workshop. Mas, se for o caso, os proprietários da versão paga do Dwarf Fortress poderão alterar os gráficos atualizados para a forma anterior no ASCII.
Mais detalhes .
Operador de atribuição de substituição
Exemplos 5, 6:Havia também um par interessante de avisos semelhantes.
V690 A classe 'JsonObject' implementa um construtor de cópia, mas não possui o operador '='. É perigoso usar essa classe. json.h 647
class JsonObject { private: .... JsonIn *jsin; .... public: JsonObject( JsonIn &jsin ); JsonObject( const JsonObject &jsobj ); JsonObject() : positions(), start( 0 ), end( 0 ), jsin( NULL ) {} ~JsonObject() { finish(); } void finish();
Essa classe possui um construtor e destrutor de cópias, no entanto, não sobrecarrega o operador de atribuição. O problema aqui é que um operador de atribuição gerado automaticamente pode atribuir apenas um ponteiro ao
JsonIn . Como resultado, os dois objetos da classe
JsonObject apontam para o mesmo
JsonIn . Não se sabe se essa situação poderia surgir em algum lugar agora, mas, em qualquer caso, esse é um rake que alguém pisará mais cedo ou mais tarde.
Um problema semelhante está presente na seguinte classe.
V690 A classe 'JsonArray' implementa um construtor de cópia, mas não possui o operador '='. É perigoso usar essa classe. json.h 820
class JsonArray { private: .... JsonIn *jsin; .... public: JsonArray( JsonIn &jsin ); JsonArray( const JsonArray &jsarr ); JsonArray() : positions(), ...., jsin( NULL ) {}; ~JsonArray() { finish(); } void finish();
Você pode ler mais sobre o perigo de uma sobrecarga de um operador de atribuição para uma classe complexa no artigo "
A lei das duas grandes " (ou na tradução deste artigo "
C ++: A lei das duas grandes ").
Exemplos 7, 8:Outro exemplo relacionado ao operador de atribuição sobrecarregado, mas desta vez estamos falando sobre sua implementação específica.
V794 O operador de atribuição deve ser protegido do caso 'this == & other'. mattack_common.h 49
class StringRef { public: .... private: friend struct StringRefTestAccess; char const* m_start; size_type m_size; char* m_data = nullptr; .... auto operator = ( StringRef const &other ) noexcept -> StringRef& { delete[] m_data; m_data = nullptr; m_start = other.m_start; m_size = other.m_size; return *this; }
O problema é que essa implementação não está protegida de atribuir o objeto a si mesma, o que é uma prática insegura. Ou seja, se uma referência a
* isso for passada para este operador, poderá ocorrer um vazamento de memória.
Um exemplo semelhante de uma sobrecarga incorreta de operador de atribuição com um efeito colateral interessante:
V794 O operador de atribuição deve ser protegido do caso de 'this == & rhs'. player_activity.cpp 38
player_activity &player_activity::operator=( const player_activity &rhs ) { type = rhs.type; .... targets.clear(); targets.reserve( rhs.targets.size() ); std::transform( rhs.targets.begin(), rhs.targets.end(), std::back_inserter( targets ), []( const item_location & e ) { return e.clone(); } ); return *this; }
Nesse caso, assim como não há verificação da atribuição do objeto a si próprio. Além disso, o vetor é preenchido. Se você tentar atribuir o objeto a si mesmo por essa sobrecarga, no campo target obteremos um vetor duplicado, cujos elementos estão corrompidos. No entanto, é
claro antes da
transformação que limpará o vetor do objeto e os dados serão perdidos.
Retiro III
Em 2008, os bagels adquiriram uma definição formal, que recebeu o nome épico de "Interpretação de Berlim". De acordo com essa definição, as principais características desses jogos são:
- Um mundo gerado aleatoriamente que aumenta o valor da repetição;
- Permadeath: se seu personagem morre, ele morre para sempre e todos os itens são perdidos;
- Passo a passo: as alterações ocorrem apenas em conjunto com a ação do jogador, até que a ação seja executada - o tempo para;
- Sobrevivência: os recursos são extremamente limitados.
Bem e mais importante: os bagels visam principalmente explorar e descobrir o mundo, procurando novas maneiras de usar objetos e passar por masmorras.
A situação habitual no Cataclysm DDA: congelada e faminta até a morte, você é atormentado pela sede e, na verdade, possui seis tentáculos em vez de pernas.
Detalhes importantes
Exemplo 9:V1028 Possível estouro. Considere converter operandos do operador 'start + maior' para o tipo 'size_t', não o resultado. worldfactory.cpp 638
void worldfactory::draw_mod_list( int &start, .... ) { .... int larger = ....; unsigned int iNum = ....; .... for( .... ) { if( iNum >= static_cast<size_t>( start ) && iNum < static_cast<size_t>( start + larger ) ) { .... } .... } .... }
Parece que o programador queria evitar o estouro. Mas trazer o resultado da adição nesse caso não faz sentido, pois o estouro ocorrerá quando os números forem adicionados e a expansão do tipo será realizada no resultado sem sentido. Para evitar essa situação, você precisa converter apenas um dos argumentos em um tipo maior:
(static_cast <size_t> (start) + maior) .
Exemplo 10:V530 O valor de retorno da função 'tamanho' deve ser utilizado. worldfactory.cpp 1340
bool worldfactory::world_need_lua_build( std::string world_name ) { #ifndef LUA .... #endif
Para tais casos, há um pequeno truque. Se a variável não for usada, em vez de tentar chamar qualquer método, você pode simplesmente escrever
(void) world_name para suprimir o aviso do compilador.
Exemplo 11:V812 Desempenho Diminuído. Uso ineficaz da função 'count'. Pode ser substituído pela chamada para a função 'localizar'. player.cpp 9600
bool player::read( int inventory_position, const bool continuous ) { .... player_activity activity; if( !continuous || !std::all_of( learners.begin(), learners.end(), [&]( std::pair<npc *, std::string> elem ) { return std::count( activity.values.begin(), activity.values.end(), elem.first->getID() ) != 0; } ) { .... } .... }
A julgar pelo fato de o resultado da
contagem ser comparado a zero, a idéia é entender se existe pelo menos um elemento necessário entre as
atividades . Mas a
contagem é forçada a percorrer todo o contêiner, pois conta todas as ocorrências do elemento. Nessa situação, será mais rápido usar o
find , que para após a primeira correspondência ser encontrada.
Exemplo 12:O seguinte erro é facilmente detectado se você souber sobre uma sutileza.
O V739 EOF não deve ser comparado com um valor do tipo 'char'. O 'ch' deve ser do tipo 'int'. json.cpp 762
void JsonIn::skip_separator() { signed char ch; .... if (ch == ',') { if( ate_separator ) { .... } .... } else if (ch == EOF) { .... }
Este é um daqueles erros que podem ser difíceis de perceber se você não souber que o
EOF está definido como -1. Portanto, se você tentar compará-lo com uma variável do tipo
char assinado , a condição será quase sempre
falsa . A única exceção é se o código de caractere for 0xFF (255). Ao comparar, esse símbolo se tornará -1 e a condição será verdadeira.
Exemplo 13:O próximo pequeno erro pode um dia se tornar crítico. Não é de admirar que esteja na lista da
CWE como a
CWE-834 . E havia, aliás, cinco deles.
V663 Loop infinito é possível. A condição 'cin.eof ()' é insuficiente para interromper o loop. Considere adicionar a chamada de função 'cin.fail ()' à expressão condicional. action.cpp 46
void parse_keymap( std::istream &keymap_txt, .... ) { while( !keymap_txt.eof() ) { .... } }
Conforme declarado no aviso, verificar o alcance do final do arquivo durante a leitura não é suficiente, você também deve verificar o erro de leitura
cin.fail () . Altere o código para uma leitura mais segura:
while( !keymap_txt.eof() ) { if(keymap_txt.fail()) { keymap_txt.clear(); keymap_txt.ignore(numeric_limits<streamsize>::max(),'\n'); break; } .... }
keymap_txt.clear () é necessário para remover o estado de erro (sinalizador) do fluxo no caso de um erro de leitura do arquivo, caso contrário, o texto não poderá ser lido mais.
keymap_txt.ignore com os
parâmetros numeric_limits <streamsize> :: max () e um caractere de controle de avanço de linha permite que você pule o restante da linha.
Há uma maneira muito mais simples de parar de ler:
while( !keymap_txt ) { .... }
Quando usado em um contexto de lógica, ele se converte em um valor equivalente a
true até que o
EOF seja alcançado.
Retiro IV
Agora, os jogos mais populares são aqueles que combinam os sinais de jogos roguelike e outros gêneros: plataformas, estratégias, etc. Esses jogos passaram a ser chamados de roguelike like ou roguelite. Tais jogos incluem títulos famosos como Don't Starve, The Binding of Isaac, FTL: Faster Than Light, Darkest Dungeon e até Diablo.
Embora, às vezes, a diferença entre roguelike e roguelite seja tão pequena que não esteja claro a que gênero o jogo pertence. Alguém acredita que Dwarf Fortress não é mais um roguelike, mas para alguém, Diablo é um bagel clássico.
Conclusão
Embora o projeto como um todo seja um exemplo de código de alta qualidade e não tenha sido possível encontrar muitos erros sérios, isso não significa que o uso da análise estática seja redundante para ele. O ponto não está nas verificações únicas que fazemos para popularizar a metodologia de análise de código estático, mas no uso regular do analisador. Então, muitos erros podem ser identificados em um estágio inicial e, portanto, reduzem o custo de corrigi-los.
Exemplo de cálculos.
O trabalho ativo está em andamento no jogo considerado e há uma comunidade ativa de modders. Além disso, é portado para muitas plataformas, incluindo iOS e Android. Então, se você está interessado neste jogo, eu recomendo que você tente!
