Dias sombrios do cataclismo adiante, análise estática e bagels

Quadro 10

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

Quadro 9

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.

Quadro 8

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.

Quadro 7

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.

Quadro 6

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(); // moves the stream to the end of the object .... void JsonObject::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(); // move the stream position to the end of the array void JsonArray::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.

Quadro 16

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.

Quadro 15

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 // Prevent unused var error when LUA and RELEASE enabled. world_name.size(); return false; } 

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) { .... } 

Quadro 3

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.

Quadro 1

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.

Quadro 2

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!

Source: https://habr.com/ru/post/pt449472/


All Articles