Ya debe haber adivinado por el título que el artículo de hoy se centrará en los errores en el código fuente del software. Pero no solo eso. Si no solo está interesado en C ++ y en leer sobre errores en el código de otros desarrolladores, sino también en cavar videojuegos inusuales y preguntarse qué son los "roguelikes" y cómo los juega, ¡bienvenido a seguir leyendo!
Mientras buscaba juegos inusuales, me topé con
Cataclysm Dark Days Ahead , que se destaca entre otros juegos gracias a sus gráficos basados en caracteres ASCII de varios colores dispuestos en el fondo negro.
Una cosa que te sorprende de este y otros juegos similares es cuánta funcionalidad está integrada en ellos. Particularmente en
Cataclysm , por ejemplo, ni siquiera puedes crear un personaje sin sentir la necesidad de buscar en Google algunas guías debido a las docenas de parámetros, rasgos y escenarios iniciales disponibles, sin mencionar las múltiples variaciones de eventos que ocurren a lo largo del juego.
Dado que es un juego con código de código abierto y uno escrito en C ++, no podríamos pasar sin verificarlo con nuestro analizador de código estático PVS-Studio, en cuyo desarrollo participo activamente. El código del proyecto es sorprendentemente de alta calidad, pero aún tiene algunos defectos menores, de los cuales hablaré en este artículo.
Ya se han comprobado muchos juegos con PVS-Studio. Puede encontrar algunos ejemplos en nuestro artículo "
Análisis estático en el desarrollo de videojuegos: Top 10 errores de software ".
La lógica
Ejemplo 1:Este ejemplo muestra un error clásico de copiar y pegar.
V501 Hay
subexpresiones idénticas a la izquierda y a la derecha de '||' 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 ) ) { .... } .... }
La misma condición se verifica dos veces. El programador copió la expresión pero olvidó modificar la copia. No estoy seguro de si se trata de un error crítico, pero el hecho es que la verificación no funciona como estaba previsto.
Otro error similar:
- V501 Hay subexpresiones idénticas 'one_in (100000 / to_turns <int> (dur))' a la izquierda y a la derecha del operador '&&'. player_hardcoded_effects.cpp 547
Ejemplo 2V728 Se puede simplificar una verificación excesiva. El '(A y B) || (! A &&! B) 'expresión es equivalente a la expresión' bool (A) == bool (B) '. Inventory_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 .... } .... }
Esta condición es lógicamente correcta, pero es demasiado complicada. Quien escribió este código debería haber tenido lástima de sus compañeros programadores que lo mantendrán. Podría reescribirse en una forma más simple:
if (left_fav == right_fav) .
Otro error similar:
- V728 Se puede simplificar una verificación excesiva. El '(A &&! B) || (! A && B) 'expresión es equivalente a la expresión' bool (A)! = Bool (B) '. iuse_actor.cpp 2653
Digresión i
Me sorprendió descubrir que los juegos que hoy se conocen con el nombre de "roguelikes" son solo representantes más moderados del antiguo género de los juegos de roguelike. Todo comenzó con el juego de culto
Rogue de 1980, que inspiró a muchos estudiantes y programadores a crear sus propios juegos con elementos similares. Supongo que también influyó mucho la comunidad del juego de mesa
DnD y sus variaciones.
Micro optimizaciones
Ejemplo 3Las advertencias de este grupo apuntan a puntos que podrían optimizarse en lugar de errores.
V801 Disminución del rendimiento. Es mejor redefinir el argumento de la segunda función como referencia. Considere reemplazar 'const ... type' con '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; }
En este código,
itype_id es en realidad un
std :: string disfrazado. Dado que el argumento se pasa como una constante de todos modos, lo que significa que es inmutable, simplemente pasar una referencia a la variable ayudaría a mejorar el rendimiento y ahorrar recursos computacionales al evitar la operación de copia. Y a pesar de que es poco probable que la cadena sea larga, copiarla cada vez sin una buena razón es una mala idea, más aún porque varias personas llaman a esta función, que, a su vez, también teclean desde afuera y tienen para copiarlo
Problemas similares
- V801 Disminución del rendimiento. Es mejor redefinir el argumento de la tercera función como referencia. Considere reemplazar 'const ... evt_filter' con 'const ... & evt_filter'. input.cpp 691
- V801 Disminución del rendimiento. Es mejor redefinir el argumento de la quinta función como referencia. Considere reemplazar 'const ... color' con 'const ... & color'. salida.h 207
- El analizador emitió un total de 32 advertencias de este tipo.
Ejemplo 4V813 Disminución del rendimiento. El argumento 'str' probablemente debería representarse como una referencia 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; }
Aunque el argumento no es constante, no cambia en el cuerpo de la función de ninguna manera. Por lo tanto, en aras de la optimización, una mejor solución sería pasarlo por referencia constante en lugar de forzar al compilador a crear copias locales.
Esta advertencia tampoco vino sola; El número total de advertencias de este tipo es 26.
Problemas similares
- V813 Disminución del rendimiento. El argumento 'mensaje' probablemente debería representarse como una referencia constante. json.cpp 1452
- V813 Disminución del rendimiento. El argumento 's' probablemente debería representarse como una referencia constante. catacharset.cpp 218
- Y así sucesivamente ...
Digresión ii
Algunos de los juegos clásicos de roguelike todavía están en desarrollo activo. Si revisa los repositorios GitHub de
Cataclysm DDA o
NetHack , verá que los cambios se envían todos los días.
NetHack es en realidad el juego más antiguo que aún se está desarrollando: se lanzó en julio de 1987 y la última versión data de 2018.
Dwarf Fortress es uno de los juegos más populares, aunque más jóvenes, del género. El desarrollo comenzó en 2002 y la primera versión se lanzó en 2006. Su lema "Perder es divertido" refleja el hecho de que es imposible ganar en este juego. En 2007,
Dwarf Fortress fue galardonado con el "Mejor juego de Roguelike del año" al votar anualmente en el sitio ASCII GAMES.
Por cierto, los fanáticos podrían estar contentos de saber que
Dwarf Fortress llegará a Steam con gráficos mejorados de 32 bits agregados por dos modders experimentados. La versión premium también obtendrá pistas de música adicionales y soporte para Steam Workshop. Los propietarios de copias pagas podrán cambiar a los gráficos ASCII antiguos si lo desean.
MásAnular el operador de asignación
Ejemplos 5, 6:Aquí hay un par de advertencias interesantes.
V690 La clase 'JsonObject' implementa un constructor de copia, pero carece del operador '='. Es peligroso usar tal clase. 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();
Esta clase tiene un constructor de copia y un destructor, pero no anula el operador de asignación. El problema es que un operador de asignación generado automáticamente puede asignar el puntero solo a
JsonIn . Como resultado, ambos objetos de la clase
JsonObject estarían apuntando al mismo
JsonIn . No puedo decir con certeza si tal situación podría ocurrir en la versión actual, pero alguien seguramente caerá en esta trampa algún día.
La próxima clase tiene un problema similar.
V690 La clase 'JsonArray' implementa un constructor de copia, pero carece del operador '='. Es peligroso usar tal clase. json.h 820
class JsonArray { private: .... JsonIn *jsin; .... public: JsonArray( JsonIn &jsin ); JsonArray( const JsonArray &jsarr ); JsonArray() : positions(), ...., jsin( NULL ) {}; ~JsonArray() { finish(); } void finish();
El peligro de no anular el operador de asignación en una clase compleja se explica en detalle en el artículo "
La ley de los dos grandes ".
Ejemplos 7, 8:Estos dos también se ocupan de la anulación del operador de asignación, pero esta vez implementaciones específicas de la misma.
V794 El operador de asignación debe estar protegido del caso 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; }
Esta implementación no tiene protección contra la posible autoasignación, lo cual es una práctica insegura. Es decir, pasar un
* esta referencia a este operador puede causar una pérdida de memoria.
Aquí hay un ejemplo similar de un operador de asignación anulado incorrectamente con un efecto secundario peculiar:
V794 El operador de asignación debe estar protegido del 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; }
Este código tampoco verifica la autoasignación y, además, tiene un vector que se debe completar. Con esta implementación del operador de asignación, la asignación de un objeto a sí mismo dará como resultado la duplicación del vector en el campo de
objetivos , y algunos de los elementos se corromperán. Sin embargo, la
transformación está precedida por
clear , que borrará el vector del objeto, lo que conducirá a la pérdida de datos.
Digresión iii
En 2008, roguelikes incluso obtuvo una definición formal conocida bajo el título épico "Interpretación de Berlín". Según él, todos estos juegos comparten los siguientes elementos:
- Mundo generado aleatoriamente, lo que aumenta la capacidad de reproducción;
- Permadeath: si tu personaje muere, muere para siempre, y todos sus objetos se pierden;
- Juego basado en turnos: cualquier cambio ocurre solo junto con las acciones del jugador; el flujo de tiempo se suspende hasta que el jugador realiza una acción;
- Supervivencia: los recursos son escasos.
Finalmente, la característica más importante de los roguelikes se centra principalmente en explorar el mundo, encontrar nuevos usos para los artículos y en el rastreo de mazmorras.
Es una situación común en
Cataclysm DDA que tu personaje termine congelado hasta los huesos, hambriento, sediento y, para colmo, reemplazando sus dos patas con seis tentáculos.
Detalles que importan
Ejemplo 9V1028 Posible desbordamiento. Considere convertir los operandos del operador 'inicio + más grande' al tipo 'size_t', no el 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 el programador quería tomar precauciones contra un desbordamiento. Sin embargo, promover el tipo de suma no hará ninguna diferencia porque el desbordamiento ocurrirá antes de eso, en el paso de agregar los valores, y la promoción se realizará sobre un valor sin sentido. Para evitar esto, solo uno de los argumentos debe convertirse a un tipo más amplio:
(static_cast <size_t> (inicio) + más grande) .
Ejemplo 10V530 Se requiere utilizar el valor de retorno de la función 'tamaño'. worldfactory.cpp 1340
bool worldfactory::world_need_lua_build( std::string world_name ) { #ifndef LUA .... #endif
Hay un truco para casos como este. Si termina con una variable no utilizada y desea suprimir la advertencia del compilador, simplemente escriba
(void) world_name en lugar de llamar a métodos en esa variable.
Ejemplo 11V812 Disminución del rendimiento. Uso ineficaz de la función 'contar'. Posiblemente puede ser reemplazado por la llamada a la función 'buscar'. 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; } ) { .... } .... }
El hecho de que el
conteo se compara con cero sugiere que el programador quería averiguar si la
actividad contenía al menos un elemento requerido. Pero
count tiene que recorrer todo el contenedor ya que cuenta todas las ocurrencias del elemento. El trabajo se puede hacer más rápido usando
find , que se detiene una vez que se ha encontrado la primera aparición.
Ejemplo 12Este error es fácil de encontrar si conoce un detalle complicado sobre el tipo
char .
V739 EOF no debe compararse con un valor del tipo 'char'. El 'ch' debe ser del tipo 'int'. json.cpp 762
void JsonIn::skip_separator() { signed char ch; .... if (ch == ',') { if( ate_separator ) { .... } .... } else if (ch == EOF) { .... }
Este es uno de los errores que no detectará fácilmente a menos que sepa que
EOF se define como -1. Por lo tanto, al compararlo con una variable de tipo con
signo char , la condición se evalúa como
falsa en casi todos los casos. La única excepción es con el carácter cuyo código es 0xFF (255). Cuando se usa en una comparación, se convertirá en -1, lo que hace que la condición sea verdadera.
Ejemplo 13Este pequeño error puede volverse crítico algún día. Hay buenas razones, después de todo, que se encuentra en la lista de
CWE como
CWE-834 . Tenga en cuenta que el proyecto ha activado esta advertencia cinco veces.
V663 Infinite loop es posible. La condición 'cin.eof ()' es insuficiente para romper el bucle. Considere agregar la llamada a la función 'cin.fail ()' a la expresión condicional. action.cpp 46
void parse_keymap( std::istream &keymap_txt, .... ) { while( !keymap_txt.eof() ) { .... } }
Como dice la advertencia, no es suficiente verificar si hay EOF al leer el archivo; también debe verificar si hay un error de entrada llamando a
cin.fail () . Arreglemos el código para hacerlo más seguro:
while( !keymap_txt.eof() ) { if(keymap_txt.fail()) { keymap_txt.clear(); keymap_txt.ignore(numeric_limits<streamsize>::max(),'\n'); break; } .... }
El propósito de
keymap_txt.clear () es borrar el estado de error (bandera) en la secuencia después de que ocurra un error de lectura para que pueda leer el resto del texto. Llamar a
keymap_txt.ignore con los parámetros
numeric_limits <streamsize> :: max () y el carácter de nueva línea le permite omitir la parte restante de la cadena.
Hay una manera mucho más simple de detener la lectura:
while( !keymap_txt ) { .... }
Cuando se pone en contexto lógico, la secuencia se convertirá en un valor equivalente a
verdadero hasta que se alcance
EOF .
Digresión iv
Los juegos más populares relacionados con roguelike de nuestro tiempo combinan los elementos de roguelikes originales y otros géneros como plataformas, estrategias, etc. Dichos juegos se conocen como "roguelike-like" o "roguelite". Entre estos se encuentran títulos tan famosos como
Don't Starve ,
The Binding of Isaac ,
FTL: Faster Than Light ,
Darkest Dungeon e incluso
Diablo .
Sin embargo, la distinción entre roguelike y roguelite a veces puede ser tan pequeña que no se puede saber con seguridad a qué categoría pertenece el juego. Algunos argumentan que
Dwarf Fortress no es un roguelike en sentido estricto, mientras que otros creen que
Diablo es un clásico juego de roguelike.
Conclusión
Aunque el proyecto demostró ser generalmente de alta calidad, con solo unos pocos defectos graves, no significa que pueda prescindir del análisis estático. El poder del análisis estático se usa regularmente en lugar de verificaciones únicas como las que hacemos para popularización. Cuando se usan regularmente, los analizadores estáticos lo ayudan a detectar errores en la primera etapa de desarrollo y, por lo tanto, los hacen más baratos de solucionar.
Ejemplos de cálculos .
El juego aún se está desarrollando intensamente, con una comunidad modder activa trabajando en él. Por cierto, se ha portado a múltiples plataformas, incluidas iOS y Android. Entonces, si estás interesado, ¡pruébalo!