Cataclysm Dark Days Menjelang, Analisis Statis, dan Bagel

Gambar 10

Kemungkinan besar, Anda sudah menebak dari judul artikel bahwa fokusnya adalah pada kesalahan dalam kode sumber. Namun ini bukan satu-satunya hal yang akan dibahas dalam artikel ini. Jika selain C ++ dan kesalahan dalam kode orang lain Anda tertarik dengan permainan yang tidak biasa dan Anda tertarik untuk mengetahui apa "bagel" ini dan apa yang mereka makan bersama mereka, selamat datang di kat!

Dalam pencarian saya untuk game yang tidak biasa, saya menemukan permainan Cataclysm Dark Days Ahead, yang berbeda dari grafik yang tidak biasa lainnya: ia diimplementasikan menggunakan karakter ASCII multi-warna pada latar belakang hitam.

Apa yang mencolok dalam game ini dan sejenisnya adalah seberapa banyak semuanya diterapkan di dalamnya. Khususnya, dalam Cataclysm, misalnya, bahkan untuk membuat karakter saya ingin mencari panduan, karena ada puluhan parameter, fitur, dan plot awal yang berbeda, belum lagi variasi acara dalam permainan itu sendiri.

Ini adalah permainan sumber terbuka, dan juga ditulis dalam C ++. Jadi tidak mungkin untuk melewati dan tidak menjalankan proyek ini melalui analisa statis PVS-Studio, dalam pengembangan yang sekarang saya terlibat aktif. Proyek itu sendiri mengejutkan saya dengan kualitas kode yang tinggi, namun masih mengandung beberapa kekurangan dan saya akan membahas beberapa di antaranya dalam artikel ini.

Sampai saat ini, dengan bantuan PVS-Studio, banyak game yang telah diuji. Misalnya, Anda dapat membaca artikel kami yang lain, " Analisis Statis di Industri Video Game: 10 Kesalahan Perangkat Lunak Teratas ".

Logika


Contoh 1:

Contoh berikut adalah kesalahan salin khas.

V501 Ada sub-ekspresi identik ke kiri dan ke kanan '||' 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 ) ) { .... } .... } 

Di sini kondisi yang sama diperiksa dua kali. Kemungkinan besar, ekspresi itu disalin dan lupa untuk mengubah sesuatu di dalamnya. Saya merasa sulit untuk mengatakan apakah kesalahan ini signifikan, tetapi pemeriksaan tidak berfungsi sebagaimana dimaksud.

Peringatan serupa:
  • V501 Ada sub-ekspresi identik 'one_in (100000 / to_turns <int> (dur))' di sebelah kiri dan di sebelah kanan operator '&&'. player_hardcoded_effects.cpp 547

Gambar 9

Contoh 2:

V728 Pemeriksaan berlebihan dapat disederhanakan. The '(A && B) || Ekspresi (! A &&! B) 'sama dengan ekspresi' 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 .... } .... } 

Tidak ada kesalahan dalam kondisi ini, tetapi tidak perlu rumit. Sebaiknya kasihan pada mereka yang harus membongkar kondisi ini, dan lebih mudah untuk menulis jika (left_fav == right_fav) .

Peringatan serupa:

  • V728 Pemeriksaan berlebihan dapat disederhanakan. The '(A &&! B) || Ekspresi (! A && B) 'sama dengan ekspresi' bool (A)! = Bool (B) '. iuse_actor.cpp 2653

Mundur saya


Ternyata menjadi penemuan bagi saya bahwa game yang sekarang disebut "bagel" hanyalah pengikut yang cukup ringan dari genre lama game roguelike. Semuanya dimulai dengan permainan Rogue kultus tahun 1980, yang menjadi panutan dan menginspirasi banyak siswa dan programmer untuk membuat game mereka sendiri. Saya pikir banyak juga yang dibawa oleh komunitas bermain peran dewan DnD dan variasinya.

Gambar 8

Optimalisasi mikro


Contoh 3:

Grup berikutnya dari penganalisa peringatan tidak menunjukkan kesalahan, tetapi kemungkinan optimasi mikro dari kode program.

V801 Menurunkan Kinerja. Lebih baik mendefinisikan kembali argumen fungsi kedua sebagai referensi. Pertimbangkan untuk mengganti 'const ... ketik' dengan 'const ... & ketik'. 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; } 

Di sini itdpe_id menyembunyikan std :: string . Karena argumennya tetap konstan, yang tidak akan membiarkannya diubah, akan lebih cepat untuk hanya meneruskan referensi variabel ke fungsi dan tidak membuang sumber daya untuk menyalin. Dan meskipun, kemungkinan besar, garis di sana akan sangat kecil, tetapi penyalinan yang konstan tanpa alasan yang jelas tidak diperlukan. Selain itu, fungsi ini dipanggil dari tempat yang berbeda, banyak di antaranya, pada gilirannya, juga mendapatkan tipe dari luar dan menyalinnya.

Peringatan serupa:

  • V801 Menurunkan Kinerja. Lebih baik mendefinisikan kembali argumen fungsi ketiga sebagai referensi. Pertimbangkan untuk mengganti 'const ... evt_filter' dengan 'const ... & evt_filter'. input.cpp 691
  • V801 Menurunkan Kinerja. Lebih baik mendefinisikan kembali argumen fungsi kelima sebagai referensi. Pertimbangkan untuk mengganti 'const ... color' dengan 'const ... & color'. output.h 207
  • Secara total, penganalisa menghasilkan 32 peringatan seperti itu.

Contoh 4:

V813 Menurunkan Kinerja. Argumen 'str' mungkin harus diterjemahkan sebagai referensi konstan. 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; } 

Dalam hal ini, argumen, meskipun tidak konstan, tidak berubah dalam tubuh fungsi. Oleh karena itu, untuk pengoptimalan, alangkah baiknya untuk mengirimkannya melalui tautan konstan, dan tidak memaksa kompiler untuk membuat salinan lokal.

Peringatan ini juga tidak tunggal, ada total 26 kasus.

Gambar 7

Peringatan serupa:

  • V813 Menurunkan Kinerja. Argumen 'pesan' mungkin harus diterjemahkan sebagai referensi konstan. json.cpp 1452
  • V813 Menurunkan Kinerja. Argumen 's' mungkin harus diterjemahkan sebagai referensi konstan. catacharset.cpp 218
  • Dan seterusnya ...

Retret II


Beberapa game roguelike klasik masih dikembangkan. Jika Anda pergi ke gudang Dac atau NetHack Cataclysm GitHub, Anda dapat melihat bahwa perubahan sedang dilakukan secara aktif setiap hari. NetHack umumnya adalah game tertua yang masih dikembangkan: dirilis pada Juli 1987, dan versi terbaru dari 2018.

Salah satu yang terkenal, bagaimanapun, permainan selanjutnya dari genre ini adalah Dwarf Fortress, dikembangkan sejak 2002 dan dirilis pertama kali pada 2006. โ€œLosing is funโ€ adalah moto permainan, yang secara akurat mencerminkan esensinya, karena tidak mungkin untuk memenangkannya. Game ini pada 2007 mendapatkan gelar game roguelike terbaik tahun ini sebagai hasil pemungutan suara, yang diadakan setiap tahun di situs web ASCII GAMES.

Gambar 6

Ngomong-ngomong, mereka yang tertarik dengan game ini mungkin tertarik dengan berita berikut. Dwarf Fortress akan dirilis di Steam dengan grafis 32-bit yang ditingkatkan. Dengan gambar yang diperbarui yang sedang dikerjakan oleh dua moderator game berpengalaman, Dwarf Fortress versi premium akan menerima trek musik tambahan dan dukungan untuk Steam Workshop. Tetapi jika ada, pemilik versi berbayar Dwarf Fortress akan dapat mengubah grafik yang diperbarui ke bentuk sebelumnya di ASCII. Lebih detail .

Operator Tugas Utama


Contoh 5, 6:

Ada juga sepasang peringatan serupa yang menarik.

V690 Kelas 'JsonObject' mengimplementasikan copy constructor, tetapi tidak memiliki operator '='. Berbahaya menggunakan kelas semacam itu. 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() { .... } .... } 

Kelas ini memiliki copy constructor dan destructor, namun, itu tidak membebani operator penugasan. Masalahnya di sini adalah bahwa operator penugasan yang dibuat secara otomatis hanya dapat menetapkan penunjuk ke JsonIn . Akibatnya, kedua objek dari kelas JsonObject menunjuk ke JsonIn yang sama. Tidak diketahui apakah situasi seperti itu dapat muncul di suatu tempat sekarang, tetapi, dalam hal apa pun, ini adalah garu yang akan diinjak seseorang cepat atau lambat.

Masalah serupa hadir di kelas berikut.

V690 Kelas 'JsonArray' mengimplementasikan copy constructor, tetapi tidak memiliki operator '='. Berbahaya menggunakan kelas semacam itu. 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() { .... } } 

Anda dapat membaca lebih lanjut tentang bahaya kurangnya kelebihan operator penugasan untuk kelas kompleks di artikel " Hukum Dua Besar " (atau dalam terjemahan artikel ini " C ++: Hukum Dua Besar ").

Contoh 7, 8:

Contoh lain terkait dengan operator penugasan yang kelebihan beban, tetapi kali ini kita berbicara tentang implementasi spesifiknya.

V794 Operator penugasan harus dilindungi dari kasus '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; } 

Masalahnya adalah bahwa implementasi ini tidak terlindungi dari menetapkan objek ke dirinya sendiri, yang merupakan praktik yang tidak aman. Artinya, jika referensi ke * ini diteruskan ke operator ini, kebocoran memori dapat terjadi.

Contoh serupa dari operator penugasan yang keliru membebani dengan efek samping yang menarik:

V794 Operator penugasan harus dilindungi dari kasus '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; } 

Dalam hal ini, sama seperti tidak ada pemeriksaan pada penugasan objek untuk dirinya sendiri. Tetapi di samping itu, vektor mengisi. Jika Anda mencoba untuk menetapkan objek kepada diri Anda sendiri dengan kelebihan seperti itu, maka di bidang target kami mendapatkan vektor berlipat ganda, beberapa di antaranya elemennya rusak. Namun, ada yang jelas sebelum mentransformasikan yang akan menghapus vektor objek dan data akan hilang.

Gambar 16

Retret III


Pada tahun 2008, bagel bahkan memperoleh definisi formal, yang diberi nama epik "Berlin Interpretation". Menurut definisi ini, fitur utama dari game tersebut adalah:

  • Dunia yang dihasilkan secara acak yang meningkatkan nilai replay;
  • Permadeath: jika karakter Anda mati, dia mati selamanya dan semua item hilang;
  • Langkah-demi-langkah: perubahan hanya terjadi bersamaan dengan aksi pemain, sampai aksi dilakukan - waktu berhenti;
  • Kelangsungan hidup: sumber daya sangat terbatas.

Baik dan yang paling penting: bagel ditujukan terutama untuk menjelajahi dan menemukan dunia, mencari cara baru untuk menggunakan objek dan melewati ruang bawah tanah.

Situasi yang biasa di DDA Cataclysm: beku dan lapar sampai mati, Anda tersiksa oleh kehausan, dan memang Anda memiliki enam tentakel alih-alih kaki.

Gambar 15

Detail penting


Contoh 9:

V1028 Kemungkinan meluap. Pertimbangkan casting operan dari operator 'mulai + lebih besar' ke tipe 'size_t', bukan hasilnya. 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 ) ) { .... } .... } .... } 

Sepertinya programmer ingin menghindari overflow. Tetapi membawa hasil penambahan dalam hal ini tidak ada gunanya, karena overflow akan terjadi ketika angka ditambahkan, dan ekspansi tipe akan dilakukan pada hasil yang tidak berarti. Untuk menghindari situasi ini, Anda hanya perlu memberikan satu argumen ke tipe yang lebih besar: (static_cast <size_t> (start) + lebih besar) .

Contoh 10:

V530 Nilai balik fungsi 'ukuran' diperlukan untuk digunakan. 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; } 

Untuk kasus seperti itu, ada sedikit trik. Jika variabel tidak digunakan, alih-alih mencoba memanggil metode apa pun, Anda bisa menulis (void) world_name untuk menekan peringatan kompiler.

Contoh 11:

V812 Menurunkan Performa. Penggunaan fungsi 'hitungan' yang tidak efektif. Itu mungkin dapat diganti dengan panggilan ke fungsi 'temukan'. 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; } ) { .... } .... } 

Dilihat oleh fakta bahwa hasil perhitungan dibandingkan dengan nol, idenya adalah untuk memahami jika setidaknya ada satu elemen yang diperlukan di antara aktivitas . Tetapi hitungan dipaksa untuk melewati seluruh wadah, karena menghitung semua kejadian elemen. Dalam situasi ini, akan lebih cepat menggunakan find , yang berhenti setelah kecocokan pertama ditemukan.

Contoh 12:

Kesalahan berikut mudah terdeteksi jika Anda tahu tentang satu kehalusan.

V739 EOF tidak boleh dibandingkan dengan nilai tipe 'char'. 'Ch' harus dari tipe 'int'. json.cpp 762

 void JsonIn::skip_separator() { signed char ch; .... if (ch == ',') { if( ate_separator ) { .... } .... } else if (ch == EOF) { .... } 

Gambar 3

Ini adalah salah satu kesalahan yang sulit untuk diketahui jika Anda tidak tahu bahwa EOF didefinisikan sebagai -1. Karenanya, jika Anda mencoba membandingkannya dengan variabel tipe char yang ditandatangani , kondisinya hampir selalu salah . Satu-satunya pengecualian adalah jika kode karakter adalah 0xFF (255). Saat membandingkan, simbol seperti itu akan berubah menjadi -1 dan kondisinya akan benar.

Contoh 13:

Kesalahan kecil berikutnya mungkin suatu hari menjadi kritis. Tidak heran itu ada di daftar CWE sebagai CWE-834 . Omong-omong, ada lima dari mereka.

V663 Infinite loop dimungkinkan. Kondisi 'cin.eof ()' tidak cukup untuk memutuskan dari loop. Pertimbangkan untuk menambahkan pemanggilan fungsi 'cin.fail ()' ke ekspresi kondisional. action.cpp 46

 void parse_keymap( std::istream &keymap_txt, .... ) { while( !keymap_txt.eof() ) { .... } } 

Seperti yang dinyatakan dalam peringatan, memeriksa untuk mencapai akhir file saat membaca tidak cukup, Anda juga harus memeriksa kesalahan baca cin.fail () . Ubah kode untuk pembacaan yang lebih aman:

 while( !keymap_txt.eof() ) { if(keymap_txt.fail()) { keymap_txt.clear(); keymap_txt.ignore(numeric_limits<streamsize>::max(),'\n'); break; } .... } 

keymap_txt.clear () diperlukan untuk menghapus status kesalahan (flag) dari aliran jika terjadi kesalahan pembacaan dari file, jika tidak teks tidak dapat dibaca lebih lanjut. keymap_txt.ignore dengan numeric_limits parameter < streamsize > :: max () dan karakter kontrol umpan baris memungkinkan Anda untuk melewati sisa baris.

Ada cara yang lebih sederhana untuk berhenti membaca:

 while( !keymap_txt ) { .... } 

Ketika digunakan dalam konteks logika, itu mengkonversi dirinya sendiri ke nilai yang setara dengan yang benar sampai EOF tercapai.

Mundur IV


Sekarang gim yang paling populer adalah gim yang menggabungkan tanda-tanda permainan roguelike dan genre lainnya: platformer, strategi, dll. Game semacam itu kemudian disebut roguelike-like atau roguelite. Game-game tersebut termasuk judul-judul terkenal seperti Don't Starve, The Binding of Isaac, FTL: Faster Than Light, Darkest Dungeon, dan bahkan Diablo.

Meskipun kadang-kadang perbedaan antara roguelike dan roguelite sangat kecil sehingga tidak jelas genre yang dimiliki permainan. Seseorang percaya bahwa Dwarf Fortress tidak lagi seperti roguelike, tetapi bagi seseorang, Diablo adalah bagel klasik.

Gambar 1

Kesimpulan


Meskipun proyek secara keseluruhan adalah contoh kode berkualitas tinggi, dan tidak mungkin menemukan banyak kesalahan serius, ini tidak berarti bahwa penggunaan analisis statis adalah berlebihan untuk itu. Intinya bukan dalam satu kali pemeriksaan yang kami lakukan untuk mempopulerkan metodologi analisis kode statis, tetapi dalam penggunaan reguler penganalisis. Kemudian banyak kesalahan dapat diidentifikasi pada tahap awal dan, oleh karena itu, mengurangi biaya untuk memperbaikinya. Contoh perhitungan.

Gambar 2

Pekerjaan aktif sedang berlangsung pada game yang dipertimbangkan, dan ada komunitas modders yang aktif. Selain itu, ini porting ke banyak platform, termasuk iOS dan Android. Jadi, jika Anda tertarik dengan game ini, saya sarankan Anda mencoba!

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


All Articles