Sie müssen bereits aus dem Titel erraten haben, dass sich der heutige Artikel auf Fehler im Software-Quellcode konzentrieren wird. Aber nicht nur das. Wenn Sie nicht nur an C ++ interessiert sind und über Fehler im Code anderer Entwickler lesen möchten, sondern auch ungewöhnliche Videospiele ausgraben und sich fragen, was "Roguelikes" sind und wie Sie sie spielen, dann lesen Sie weiter!
Auf der Suche nach ungewöhnlichen Spielen bin ich auf
Cataclysm Dark Days Ahead gestoßen, das sich unter anderem durch seine Grafiken auszeichnet, die auf ASCII-Zeichen in verschiedenen Farben basieren, die auf dem schwarzen Hintergrund angeordnet sind.
Eine Sache, die Sie an diesem und anderen ähnlichen Spielen überrascht, ist, wie viel Funktionalität in sie eingebaut ist. Insbesondere in
Cataclysm können Sie beispielsweise nicht einmal einen Charakter erstellen, ohne den Drang zu verspüren, einige Anleitungen zu googeln, da Dutzende von Parametern, Merkmalen und Anfangsszenarien verfügbar sind, ganz zu schweigen von den zahlreichen Variationen von Ereignissen, die während des Spiels auftreten.
Da es sich um ein Spiel mit Open-Source-Code handelt, das in C ++ geschrieben wurde, konnten wir nicht vorbeigehen, ohne es mit unserem statischen Code-Analysator PVS-Studio zu überprüfen, an dessen Entwicklung ich aktiv teilnehme. Der Code des Projekts ist überraschend hochwertig, weist jedoch noch einige kleinere Mängel auf, von denen ich in diesem Artikel auf einige eingehen werde.
Viele Spiele wurden bereits mit PVS-Studio überprüft. Einige Beispiele finden Sie in unserem Artikel "
Statische Analyse in der Videospielentwicklung: Top 10 Softwarefehler ".
Logik
Beispiel 1:Dieses Beispiel zeigt einen klassischen Fehler beim Kopieren und Einfügen.
V501 Links und rechts vom '||' befinden sich identische Unterausdrücke. Operator: 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 ) ) { .... } .... }
Der gleiche Zustand wird zweimal überprüft. Der Programmierer hat den Ausdruck kopiert, aber vergessen, die Kopie zu ändern. Ich bin nicht sicher, ob dies ein kritischer Fehler ist, aber Tatsache ist, dass die Überprüfung nicht so funktioniert, wie es beabsichtigt war.
Ein weiterer ähnlicher Fehler:
- V501 Links und rechts vom Operator '&&' befinden sich identische Unterausdrücke 'one_in (100000 / to_turns <int> (dur))'. player_hardcoded_effects.cpp 547
Beispiel 2:V728 Eine übermäßige Überprüfung kann vereinfacht werden. Die '(A && B) || (! A &&! B) 'Ausdruck entspricht dem Ausdruck' bool (A) == bool (B) '. inventar_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 .... } .... }
Dieser Zustand ist logisch korrekt, aber zu kompliziert. Wer diesen Code geschrieben hat, sollte Mitleid mit seinen Programmierkollegen haben, die ihn pflegen werden. Es könnte in einer einfacheren Form umgeschrieben werden:
if (left_fav == right_fav) .
Ein weiterer ähnlicher Fehler:
- V728 Eine übermäßige Überprüfung kann vereinfacht werden. Das '(A &&! B) || (! A && B) 'Ausdruck entspricht dem Ausdruck' bool (A)! = Bool (B) '. iuse_actor.cpp 2653
Exkurs i
Ich war überrascht zu entdecken, dass Spiele, die heute unter dem Namen "Roguelikes" bekannt sind, nur moderatere Vertreter des alten Genres der Roguelike-Spiele sind. Alles begann mit dem Kult-Spiel
Rogue von 1980, das viele Studenten und Programmierer dazu inspirierte, ihre eigenen Spiele mit ähnlichen Elementen zu entwickeln. Ich denke, viel Einfluss kam auch von der Community des Tabletop-Spiels
DnD und seinen Variationen.
Mikrooptimierungen
Beispiel 3:Warnungen dieser Gruppe weisen auf Stellen hin, die möglicherweise optimiert werden könnten, anstatt auf Fehler.
V801 Leistungsminderung. Es ist besser, das zweite Funktionsargument als Referenz neu zu definieren. Ersetzen Sie 'const ... type' durch '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; }
In diesem Code ist
itype_id tatsächlich ein getarnter
std :: string . Da das Argument ohnehin als Konstante übergeben wird, was bedeutet, dass es unveränderlich ist, würde die einfache Übergabe eines Verweises auf die Variable dazu beitragen, die Leistung zu verbessern und Rechenressourcen zu sparen, indem der Kopiervorgang vermieden wird. Und obwohl es unwahrscheinlich ist, dass die Zeichenfolge lang ist, ist es eine schlechte Idee, sie jedes Mal ohne guten Grund zu kopieren - zumal diese Funktion von verschiedenen Aufrufern aufgerufen wird, die wiederum auch von außen typisiert werden und haben um es zu kopieren.
Ähnliche Probleme:
- V801 Leistungsminderung. Es ist besser, das dritte Funktionsargument als Referenz neu zu definieren. Ersetzen Sie 'const ... evt_filter' durch 'const ... & evt_filter'. input.cpp 691
- V801 Leistungsminderung. Es ist besser, das fünfte Funktionsargument als Referenz neu zu definieren. Ersetzen Sie 'const ... color' durch 'const ... & color'. output.h 207
- Der Analysator gab insgesamt 32 Warnungen dieses Typs aus.
Beispiel 4:V813 Leistungsminderung. Das Argument 'str' sollte wahrscheinlich als konstante Referenz wiedergegeben werden. 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; }
Obwohl das Argument nicht konstant ist, ändert es sich in keiner Weise im Funktionskörper. Aus Gründen der Optimierung wäre es daher eine bessere Lösung, diese als konstante Referenz zu übergeben, anstatt den Compiler zu zwingen, lokale Kopien zu erstellen.
Diese Warnung kam auch nicht alleine; Die Gesamtzahl der Warnungen dieses Typs beträgt 26.
Ähnliche Probleme:
- V813 Leistungsminderung. Das Argument 'message' sollte wahrscheinlich als konstante Referenz gerendert werden. json.cpp 1452
- V813 Leistungsminderung. Das Argument 's' sollte wahrscheinlich als konstante Referenz wiedergegeben werden. catacharset.cpp 218
- Und so weiter ...
Exkurs ii
Einige der klassischen Roguelike-Spiele befinden sich noch in der aktiven Entwicklung. Wenn Sie die GitHub-Repositorys von
Cataclysm DDA oder
NetHack überprüfen, werden Sie
feststellen , dass Änderungen jeden Tag übermittelt werden.
NetHack ist tatsächlich das älteste Spiel, das noch entwickelt wird: Es wurde im Juli 1987 veröffentlicht und die letzte Version stammt aus dem Jahr 2018.
Dwarf Fortress ist eines der beliebtesten - wenn auch jüngeren - Spiele des Genres. Die Entwicklung begann im Jahr 2002 und die erste Version wurde im Jahr 2006 veröffentlicht. Das Motto "Verlieren macht Spaß" spiegelt die Tatsache wider, dass es unmöglich ist, in diesem Spiel zu gewinnen. Im Jahr 2007 wurde die
Zwergenfestung durch eine jährliche Abstimmung auf der ASCII GAMES-Website als "Bestes Roguelike-Spiel des Jahres" ausgezeichnet.
Übrigens könnten Fans froh sein zu wissen, dass
Dwarf Fortress mit verbesserten 32-Bit-Grafiken, die von zwei erfahrenen Moddern hinzugefügt wurden, zu Steam kommt. Die Premium-Version erhält außerdem zusätzliche Musiktitel und Steam Workshop-Unterstützung. Besitzer von kostenpflichtigen Kopien können auf Wunsch zu den alten ASCII-Grafiken wechseln.
MehrÜberschreiben des Zuweisungsoperators
Beispiele 5, 6:Hier sind einige interessante Warnungen.
V690 Die Klasse 'JsonObject' implementiert einen Kopierkonstruktor, es fehlt jedoch der Operator '='. Es ist gefährlich, eine solche Klasse zu benutzen. 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();
Diese Klasse verfügt über einen Kopierkonstruktor und einen Destruktor, überschreibt jedoch den Zuweisungsoperator nicht. Das Problem ist, dass ein automatisch generierter Zuweisungsoperator den Zeiger nur
JsonIn zuweisen kann. Infolgedessen würden beide Objekte der Klasse
JsonObject auf dasselbe
JsonIn verweisen . Ich kann nicht sicher sagen, ob eine solche Situation in der aktuellen Version auftreten könnte, aber eines Tages wird sicherlich jemand in diese Falle tappen.
Die nächste Klasse hat ein ähnliches Problem.
V690 Die Klasse 'JsonArray' implementiert einen Kopierkonstruktor, es fehlt jedoch der Operator '='. Es ist gefährlich, eine solche Klasse zu benutzen. json.h 820
class JsonArray { private: .... JsonIn *jsin; .... public: JsonArray( JsonIn &jsin ); JsonArray( const JsonArray &jsarr ); JsonArray() : positions(), ...., jsin( NULL ) {}; ~JsonArray() { finish(); } void finish();
Die Gefahr, den Zuweisungsoperator in einer komplexen Klasse nicht zu überschreiben, wird im Artikel "
Das Gesetz der großen Zwei " ausführlich erläutert.
Beispiele 7, 8:Diese beiden befassen sich auch mit dem Überschreiben von Zuweisungsoperatoren, diesmal jedoch mit spezifischen Implementierungen.
V794 Der Zuweisungsoperator sollte vor dem Fall 'this == & other' geschützt werden. 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; }
Diese Implementierung bietet keinen Schutz vor einer möglichen Selbstzuweisung, was eine unsichere Praxis ist. Das heißt, das Übergeben eines
* dieser Referenz an diesen Operator kann zu einem Speicherverlust führen.
Hier ist ein ähnliches Beispiel für einen nicht ordnungsgemäß überschriebenen Zuweisungsoperator mit einer besonderen Nebenwirkung:
V794 Der Zuweisungsoperator sollte vor dem Fall 'this == & rhs' geschützt werden. 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; }
Dieser Code hat auch keine Prüfung gegen Selbstzuweisung und zusätzlich einen zu füllenden Vektor. Bei dieser Implementierung des Zuweisungsoperators führt das Zuweisen eines Objekts zu sich selbst zu einer Verdoppelung des Vektors im
Zielfeld , wobei einige der Elemente beschädigt werden. Der
Transformation geht jedoch
clear voraus, wodurch der Vektor des Objekts gelöscht wird, was zu Datenverlust führt.
Exkurs iii
Im Jahr 2008 erhielten Roguelikes sogar eine formale Definition, die unter dem epischen Titel "Berlin Interpretation" bekannt ist. Demnach teilen alle diese Spiele die folgenden Elemente:
- Zufällig erzeugte Welt, die die Wiederspielbarkeit erhöht;
- Permadeath: Wenn dein Charakter stirbt, stirbt er für immer und alle seine Gegenstände gehen verloren.
- Rundenbasiertes Gameplay: Änderungen treten nur zusammen mit den Aktionen des Spielers auf. Der Zeitfluss wird unterbrochen, bis der Spieler eine Aktion ausführt.
- Überleben: Ressourcen sind knapp.
Schließlich ist das wichtigste Merkmal von Roguelikes, sich hauptsächlich darauf zu konzentrieren, die Welt zu erkunden, neue Verwendungsmöglichkeiten für Gegenstände zu finden und Dungeons zu kriechen.
In
Cataclysm DDA ist es eine häufige Situation, dass Ihr Charakter bis auf die Knochen gefroren, hungrig und durstig ist und, um das Ganze abzurunden, seine beiden Beine durch sechs Tentakel ersetzt wird.
Details, die wichtig sind
Beispiel 9:V1028 Möglicher Überlauf. Ziehen Sie in Betracht, Operanden des Operators 'start + large' in den Typ 'size_t' umzuwandeln, nicht in das Ergebnis. 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 ) ) { .... } .... } .... }
Es sieht so aus, als wollte der Programmierer Vorsichtsmaßnahmen gegen einen Überlauf treffen. Das Heraufstufen des Summentyps macht jedoch keinen Unterschied, da der Überlauf vorher beim Hinzufügen der Werte auftritt und das Heraufstufen über einen bedeutungslosen Wert erfolgt. Um dies zu vermeiden, sollte nur eines der Argumente in einen breiteren Typ umgewandelt werden:
(static_cast <size_t> (start) + größer) .
Beispiel 10:V530 Der Rückgabewert der Funktion 'Größe' muss verwendet werden. worldfactory.cpp 1340
bool worldfactory::world_need_lua_build( std::string world_name ) { #ifndef LUA .... #endif
Für solche Fälle gibt es einen Trick. Wenn Sie am Ende eine nicht verwendete Variable haben und die Compiler-Warnung unterdrücken möchten, schreiben Sie einfach
(void) world_name, anstatt Methoden für diese Variable aufzurufen.
Beispiel 11:V812 Leistungsminderung. Ineffektive Verwendung der Zählfunktion. Es kann möglicherweise durch den Aufruf der Funktion 'find' ersetzt werden. 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; } ) { .... } .... }
Die Tatsache, dass
count mit null verglichen wird, legt nahe, dass der Programmierer herausfinden wollte, ob die
Aktivität mindestens ein erforderliches Element enthält. Die
Zählung muss jedoch durch den gesamten Container laufen, da alle Vorkommen des Elements gezählt werden. Die Arbeit könnte schneller erledigt werden, indem
find verwendet wird , das stoppt, sobald das erste Vorkommen gefunden wurde.
Beispiel 12:Dieser Fehler ist leicht zu finden, wenn Sie ein schwieriges Detail über den
Zeichentyp kennen .
V739 EOF sollte nicht mit einem Wert vom Typ 'char' verglichen werden. Das 'ch' sollte vom Typ 'int' sein. json.cpp 762
void JsonIn::skip_separator() { signed char ch; .... if (ch == ',') { if( ate_separator ) { .... } .... } else if (ch == EOF) { .... }
Dies ist einer der Fehler, die Sie nur dann leicht erkennen können, wenn Sie wissen, dass
EOF als -1 definiert ist. Wenn Sie es mit einer Variablen vom Typ
char mit Vorzeichen vergleichen, wird die Bedingung daher in fast allen Fällen als
falsch ausgewertet. Die einzige Ausnahme ist das Zeichen, dessen Code 0xFF (255) ist. Bei Verwendung in einem Vergleich wird -1 angezeigt, wodurch die Bedingung erfüllt wird.
Beispiel 13:Dieser kleine Fehler kann eines Tages kritisch werden. Es gibt schließlich gute Gründe dafür, dass es auf der CWE-Liste als
CWE-834 steht . Beachten Sie, dass das Projekt diese Warnung fünfmal ausgelöst hat.
V663 Endlosschleife ist möglich. Die Bedingung 'cin.eof ()' reicht nicht aus, um die Schleife zu verlassen. Fügen Sie dem bedingten Ausdruck möglicherweise den Funktionsaufruf 'cin.fail ()' hinzu. action.cpp 46
void parse_keymap( std::istream &keymap_txt, .... ) { while( !keymap_txt.eof() ) { .... } }
Wie in der Warnung angegeben, reicht es nicht aus, beim Lesen aus der Datei nach EOF zu
suchen . Sie müssen auch nach einem Eingabefehler
suchen, indem Sie
cin.fail () aufrufen. Korrigieren wir den Code, um ihn sicherer zu machen:
while( !keymap_txt.eof() ) { if(keymap_txt.fail()) { keymap_txt.clear(); keymap_txt.ignore(numeric_limits<streamsize>::max(),'\n'); break; } .... }
Der Zweck von
keymap_txt.clear () besteht darin, den Fehlerstatus (Flag) im Stream zu löschen, nachdem ein Lesefehler
aufgetreten ist, damit Sie den Rest des Textes lesen können.
Wenn Sie
keymap_txt.ignore mit den Parametern
numeric_limits <streamsize> :: max () und Newline-Zeichen aufrufen, können Sie den verbleibenden Teil der Zeichenfolge überspringen.
Es gibt eine viel einfachere Möglichkeit, das Lesen zu stoppen:
while( !keymap_txt ) { .... }
Im logischen Kontext wandelt sich der Stream in einen Wert um, der
true entspricht, bis
EOF erreicht ist.
Exkurs iv
Die beliebtesten Roguelike-bezogenen Spiele unserer Zeit kombinieren die Elemente von Original-Roguelike-Spielen und anderen Genres wie Plattformspielern, Strategien usw. Solche Spiele sind als "roguelike-like" oder "roguelite" bekannt geworden. Unter diesen sind so berühmte Titel wie
Don't Starve ,
Die Bindung von Isaac ,
FTL: Schneller als Licht ,
Darkest Dungeon und sogar
Diablo .
Der Unterschied zwischen Roguelike und Roguelite kann jedoch manchmal so gering sein, dass man nicht genau sagen kann, in welche Kategorie das Spiel gehört. Einige argumentieren, dass die
Zwergenfestung im engeren Sinne kein Roguelike ist, während andere glauben, dass
Diablo ein klassisches Roguelike-Spiel ist.
Fazit
Obwohl sich das Projekt im Allgemeinen als qualitativ hochwertig erwiesen hat und nur wenige schwerwiegende Mängel aufweist, bedeutet dies nicht, dass es auf statische Analysen verzichten kann. Die Möglichkeiten der statischen Analyse werden regelmäßig genutzt und nicht einmalig überprüft, wie wir sie für die Popularisierung durchführen. Bei regelmäßiger Verwendung helfen Ihnen statische Analysegeräte dabei, Fehler in der frühesten Entwicklungsphase zu erkennen und sie daher billiger zu beheben.
Beispielberechnungen .
Das Spiel wird noch intensiv entwickelt und von einer aktiven Modder-Community bearbeitet. Übrigens wurde es auf mehrere Plattformen portiert, darunter iOS und Android. Also, wenn Sie interessiert sind, probieren Sie es aus!