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