Très probablement, d'après le titre de l'article, vous avez déjà deviné que l'accent est mis sur les erreurs dans le code source. Mais ce n'est pas la seule chose qui sera discutée dans cet article. Si en plus du C ++ et des erreurs dans le code de quelqu'un d'autre, vous êtes attiré par des jeux inhabituels et vous souhaitez savoir ce que sont ces "bagels" et ce qu'ils mangent avec eux, bienvenue au chat!
Dans ma recherche de jeux inhabituels, je suis tombé sur un jeu Cataclysm Dark Days Ahead, qui diffère des autres graphiques inhabituels: il est mis en œuvre en utilisant des caractères ASCII multicolores sur fond noir.
Ce qui frappe dans ce jeu et son acabit, c'est combien tout est implémenté en eux. Plus précisément, dans Cataclysm, par exemple, même pour créer un personnage, je veux rechercher des guides, car il existe des dizaines de paramètres, de fonctionnalités et de tracés initiaux différents, sans parler des variations d'événements dans le jeu lui-même.
Il s'agit d'un jeu open source, également écrit en C ++. Il était donc impossible de passer à côté et de ne pas exécuter ce projet via l'analyseur statique PVS-Studio, dans le développement duquel je suis maintenant activement impliqué. Le projet lui-même m'a surpris par la haute qualité du code, cependant, il contient encore quelques défauts et j'en discuterai plusieurs dans cet article.
À ce jour, de nombreux jeux ont été testés avec PVS-Studio. Par exemple, vous pouvez lire notre autre article, «
Analyse statique dans l'industrie du jeu vidéo: les 10 principales erreurs logicielles ».
La logique
Exemple 1:L'exemple suivant est une erreur de copie typique.
V501 Il existe des sous-expressions identiques à gauche et à droite de '||' opérateur: 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 ) ) { .... } .... }
Ici, la même condition est vérifiée deux fois. Très probablement, l'expression a été copiée et a oublié de changer quelque chose. J'ai du mal à dire si cette erreur est importante, mais la vérification ne fonctionne pas comme prévu.
Un avertissement similaire:
- V501 Il existe des sous-expressions identiques «one_in (100000 / to_turns <int> (dur))» à gauche et à droite de l'opérateur «&&». player_hardcoded_effects.cpp 547
Exemple 2:V728 Un contrôle excessif peut être simplifié. Le '(A && B) || (! A &&! B) 'est équivalente à l'expression' bool (A) == bool (B) '. inventaire_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 .... } .... }
Il n'y a pas d'erreur dans la condition, mais c'est inutilement compliqué. Il vaudrait la peine de prendre pitié de ceux qui doivent démonter cette condition, et il est plus facile d'écrire
if (left_fav == right_fav) .
Un avertissement similaire:
- V728 Un contrôle excessif peut être simplifié. Le '(A &&! B) || (! A && B) 'est équivalente à l'expression' bool (A)! = Bool (B) '. iuse_actor.cpp 2653
Retraite I
Il s'est avéré être une découverte pour moi que les jeux qui sont aujourd'hui appelés «bagels» ne sont que des adeptes assez légers de l'ancien genre des jeux roguelike. Tout a commencé avec le jeu culte Rogue de 1980, qui est devenu un modèle et a inspiré de nombreux étudiants et programmeurs à créer leurs propres jeux. Je pense que beaucoup a également été apporté par la communauté des jeux de rôle du conseil d'administration de DnD et ses variantes.
Microoptimisation
Exemple 3:Le prochain groupe d'avertissements de l'analyseur n'indique pas une erreur, mais la possibilité d'une microoptimisation du code du programme.
V801 Diminution des performances. Il est préférable de redéfinir le deuxième argument de fonction comme référence. Pensez à remplacer "const ... type" par "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; }
Ici,
itdpe_id masque
std :: string . Étant donné que l'argument est toujours passé constant, ce qui ne permettra pas de le modifier, il serait plus rapide de simplement passer une référence de variable à la fonction et de ne pas gaspiller de ressources lors de la copie. Et bien que, très probablement, la ligne y soit très petite, mais une copie constante sans raison apparente n'est pas nécessaire. De plus, cette fonction est appelée à partir de différents endroits, dont beaucoup, à leur tour, obtiennent également le
type de l'extérieur et le copient.
Avertissements similaires:
- V801 Diminution des performances. Il est préférable de redéfinir le troisième argument de fonction comme référence. Pensez à remplacer 'const ... evt_filter' par 'const ... & evt_filter'. input.cpp 691
- V801 Diminution des performances. Il est préférable de redéfinir le cinquième argument de fonction comme référence. Pensez à remplacer «const ... color» par «const ... & color». output.h 207
- Au total, l'analyseur a généré 32 avertissements de ce type.
Exemple 4:V813 Performances
réduites . L'argument 'str' devrait probablement être rendu comme une référence 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; }
Dans ce cas, l'argument, bien que non constant, ne change pas dans le corps de la fonction. Par conséquent, pour l'optimisation, il serait bien de le passer par un lien constant et de ne pas forcer le compilateur à créer des copies locales.
Cet avertissement n'était pas non plus unique, il y avait 26 cas au total.
Avertissements similaires:
- V813 Performances réduites. L'argument 'message' devrait probablement être restitué comme une référence constante. json.cpp 1452
- V813 Performances réduites. L'argument 's' devrait probablement être rendu comme une référence constante. catacharset.cpp 218
- Et ainsi de suite ...
Retraite II
Certains des jeux roguelike classiques sont toujours en cours de développement. Si vous accédez aux référentiels GitHub Cataclysm DDA ou NetHack, vous pouvez voir que des modifications sont activement apportées chaque jour. NetHack est généralement le plus ancien jeu en cours de développement: il est sorti en juillet 1987 et la dernière version date de 2018.
Cependant, l'un des jeux les plus célèbres de ce genre est Dwarf Fortress, développé depuis 2002 et sorti pour la première fois en 2006. «Perdre, c'est amusant» est la devise du jeu, qui reflète fidèlement son essence, car il est impossible de la gagner. Ce jeu en 2007 a remporté le titre de meilleur jeu roguelike de l'année à la suite du vote, qui a lieu chaque année sur le site Web ASCII GAMES.
Par ailleurs, ceux qui sont intéressés par ce jeu peuvent être intéressés par les nouvelles suivantes. Dwarf Fortress sortira sur Steam avec des graphismes 32 bits améliorés. Avec une image mise à jour sur laquelle travaillent deux modérateurs de jeux expérimentés, la version premium de Dwarf Fortress recevra des pistes musicales supplémentaires et un support pour Steam Workshop. Mais si quoi que ce soit, les propriétaires de la version payante de Dwarf Fortress pourront changer les graphiques mis à jour au format précédent en ASCII.
Plus de détails .
Remplacement de l'opérateur d'affectation
Exemples 5, 6:Il y avait également une paire intéressante d'avertissements similaires.
V690 La classe 'JsonObject' implémente un constructeur de copie, mais il manque l'opérateur '='. Il est dangereux d'utiliser une telle 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();
Cette classe a un constructeur et un destructeur de copie, cependant, elle ne surcharge pas l'opérateur d'affectation. Le problème ici est qu'un opérateur d'affectation généré automatiquement ne peut affecter qu'un pointeur à
JsonIn . Par conséquent, les deux objets de la classe
JsonObject pointent vers le même
JsonIn . On ne sait pas si une telle situation pourrait se produire quelque part maintenant, mais, dans tous les cas, c'est un rake sur lequel quelqu'un va marcher tôt ou tard.
Un problème similaire est présent dans la classe suivante.
V690 La classe 'JsonArray' implémente un constructeur de copie, mais il manque l'opérateur '='. Il est dangereux d'utiliser une telle 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();
Vous pouvez en savoir plus sur le danger d'un manque de surcharge d'un opérateur d'affectation pour une classe complexe dans l'article "
The Law of The Big Two " (ou dans la traduction de cet article "
C ++: The Big Two Law ").
Exemples 7, 8:Un autre exemple lié à l'opérateur d'affectation surchargé, mais cette fois, nous parlons de sa mise en œuvre spécifique.
V794 L'opérateur d'affectation doit être protégé contre le cas de '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; }
Le problème est que cette implémentation n'est pas protégée contre l'affectation de l'objet à lui-même, ce qui est une pratique dangereuse. En d'autres termes, si une référence à
* this est transmise à cet opérateur, une fuite de mémoire peut se produire.
Un exemple similaire de surcharge d'opérateur d'affectation erronée avec un effet secondaire intéressant:
V794 L'opérateur d'affectation doit être protégé contre le cas 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; }
Dans ce cas, tout comme il n'y a pas de contrôle sur l'affectation de l'objet à lui-même. Mais en plus, le vecteur se remplit. Si vous essayez de vous assigner l'objet par une telle surcharge, alors dans le champ
cibles , nous obtenons un vecteur doublé, dont certains éléments sont corrompus. Cependant, il y a
clair avant la
transformation qui effacera le vecteur de l'objet et les données seront perdues.
Retraite III
En 2008, les bagels ont même acquis une définition formelle, qui a reçu le nom épique «Interprétation de Berlin». Selon cette définition, les principales caractéristiques de ces jeux sont:
- Un monde généré aléatoirement qui augmente la valeur de relecture;
- Permadeath: si votre personnage meurt, il meurt pour toujours et tous les objets sont perdus;
- Pas à pas: les changements ne se produisent qu’en même temps que l’action du joueur, jusqu’à ce que l’action soit exécutée - le temps s’arrête;
- Survie: les ressources sont extrêmement limitées.
Eh bien et surtout: les bagels visent principalement à explorer et découvrir le monde, à chercher de nouvelles façons d'utiliser des objets et de parcourir des donjons.
La situation habituelle dans Cataclysm DDA: gelé et affamé à mort, vous êtes tourmenté par la soif, et vous avez en effet six tentacules au lieu de jambes.
Détails importants
Exemple 9:V1028 Débordement possible. Envisagez de transposer des opérandes de l'opérateur 'start + large' en type 'size_t', pas le résultat. 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 ) ) { .... } .... } .... }
Il semble que le programmeur ait voulu éviter les débordements. Mais apporter le résultat de l'addition dans ce cas est inutile, car un débordement se produira lorsque les nombres seront ajoutés et une expansion de type sera effectuée sur le résultat sans signification. Afin d'éviter cette situation, vous devez
convertir un seul des arguments en un type plus grand:
(static_cast <size_t> (start) + large) .
Exemple 10:V530 La valeur de retour de la fonction 'taille' doit être utilisée. worldfactory.cpp 1340
bool worldfactory::world_need_lua_build( std::string world_name ) { #ifndef LUA .... #endif
Pour de tels cas, il y a une petite astuce. Si la variable n'est pas utilisée, au lieu d'essayer d'appeler n'importe quelle méthode, vous pouvez simplement écrire
(void) world_name pour supprimer l'avertissement du compilateur.
Exemple 11:V812 Performances
réduites . Utilisation inefficace de la fonction «comptage». Il peut éventuellement être remplacé par l'appel à la fonction 'find'. 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 en juger par le fait que le résultat du
comptage est comparé à zéro, l'idée est de comprendre s'il y a au moins un élément requis parmi l'
activité . Mais
count est obligé de parcourir tout le conteneur, car il compte toutes les occurrences de l'élément. Dans cette situation, il sera plus rapide d'utiliser
find , qui s'arrête une fois la première correspondance trouvée.
Exemple 12:L'erreur suivante est facilement détectée si vous connaissez une subtilité.
V739 EOF ne doit pas être comparé à une valeur de type 'char'. Le «ch» doit être de type «int». json.cpp 762
void JsonIn::skip_separator() { signed char ch; .... if (ch == ',') { if( ate_separator ) { .... } .... } else if (ch == EOF) { .... }
C'est l'une de ces erreurs qui peut être difficile à remarquer si vous ne savez pas que
EOF est défini comme -1. Par conséquent, si vous essayez de le comparer avec une variable de type
signé char , la condition est presque toujours
fausse . La seule exception est si le code de caractère est 0xFF (255). Lors de la comparaison, un tel symbole se transformera en -1 et la condition sera vraie.
Exemple 13:La prochaine petite erreur pourrait un jour devenir critique. Pas étonnant qu'il figure sur la liste CWE en tant que
CWE-834 . Et il y en avait d'ailleurs cinq.
La boucle infinie
V663 est possible. La condition «cin.eof ()» est insuffisante pour rompre la boucle. Pensez à ajouter l'appel de fonction 'cin.fail ()' à l'expression conditionnelle. action.cpp 46
void parse_keymap( std::istream &keymap_txt, .... ) { while( !keymap_txt.eof() ) { .... } }
Comme indiqué dans l'avertissement, la vérification pour atteindre la fin du fichier pendant la lecture ne suffit pas, vous devez également vérifier l'erreur de lecture
cin.fail () . Modifiez le code pour une lecture plus sécurisée:
while( !keymap_txt.eof() ) { if(keymap_txt.fail()) { keymap_txt.clear(); keymap_txt.ignore(numeric_limits<streamsize>::max(),'\n'); break; } .... }
keymap_txt.clear () est nécessaire pour supprimer l'état d'erreur (indicateur) du flux en cas d'erreur de lecture du fichier, sinon le texte ne peut pas être lu plus avant.
keymap_txt.ignore avec les
paramètres numeric_limits <streamsize> :: max () et un caractère de contrôle de saut de ligne vous permet de sauter le reste de la ligne.
Il existe un moyen beaucoup plus simple d'arrêter la lecture:
while( !keymap_txt ) { .... }
Lorsqu'il est utilisé dans un contexte de logique, il se convertit en une valeur équivalente à
true jusqu'à ce que
EOF soit atteint.
Retraite IV
Maintenant, les jeux les plus populaires sont ceux qui combinent les signes des jeux roguelike et d'autres genres: les jeux de plateforme, les stratégies, etc. Ces jeux sont maintenant appelés roguelike-like ou roguelite. Ces jeux incluent des titres célèbres tels que Don't Starve, The Binding of Isaac, FTL: Faster Than Light, Darkest Dungeon et même Diablo.
Bien que la différence entre roguelike et roguelite soit parfois si petite qu'il n'est pas clair à quel genre le jeu appartient. Quelqu'un croit que Dwarf Fortress n'est plus un roguelike, mais pour quelqu'un, Diablo est un bagel classique.
Conclusion
Bien que le projet dans son ensemble soit un exemple de code de haute qualité et qu'il n'ait pas été possible de trouver de nombreuses erreurs graves, cela ne signifie pas que l'utilisation de l'analyse statique est redondante pour lui. Le point n'est pas dans les contrôles ponctuels que nous faisons afin de vulgariser la méthodologie de l'analyse de code statique, mais dans l'utilisation régulière de l'analyseur. De nombreuses erreurs peuvent alors être identifiées à un stade précoce et, par conséquent, réduire le coût de leur correction.
Exemple de calculs.
Un travail actif est en cours sur le jeu considéré et il existe une communauté active de moddeurs. De plus, il est porté sur de nombreuses plateformes, dont iOS et Android. Donc, si vous êtes intéressé par ce jeu, je vous recommande d'essayer!
