Dalam proses pemindahan ke judul yang sudah lama ditunggu-tunggu sebagai
Pemimpin Senior C ++ Over-Engineer , tahun lalu saya memutuskan untuk menulis ulang permainan yang saya kembangkan selama jam kerja (Candy Crush Saga), menggunakan saripati C ++ modern (C ++ 17). Maka lahirlah
Meta Crush Saga : sebuah
game yang berjalan pada tahap kompilasi . Saya sangat terinspirasi oleh permainan
Nibbler Matt Birner, yang menggunakan pemrograman murni pada templat untuk menciptakan kembali Snake yang terkenal dengan Nokia 3310.
“Gim seperti apa yang
sedang dijalankan pada tahap kompilasi ?”, “Bagaimana tampilannya?”, “Fungsi apa dari
C ++ 17 yang Anda gunakan dalam proyek ini?”, “Apa yang Anda pelajari?” - Pertanyaan serupa mungkin muncul di benak Anda. Untuk menjawabnya, Anda harus membaca seluruh posting, atau memasang kemalasan batin Anda dan menonton versi video dari posting - laporan saya dari
acara Meetup di Stockholm:
Catatan: demi kesehatan mental Anda dan karena
kesalahan manusia , beberapa fakta alternatif diberikan dalam artikel ini.
Gim yang berjalan pada waktu kompilasi?
Saya pikir untuk memahami apa yang saya maksudkan dengan "konsep" sebuah
game yang dieksekusi pada tahap kompilasi , Anda perlu membandingkan siklus hidup dari game semacam itu dengan siklus hidup dari game biasa.
Siklus hidup dari game reguler:
Sebagai pengembang reguler game dengan kehidupan normal, bekerja pada pekerjaan reguler dengan tingkat kesehatan mental normal, Anda biasanya memulai dengan menulis
logika game dalam bahasa favorit Anda (dalam C ++, tentu saja!), Dan kemudian jalankan
kompiler untuk mengonversi ini, terlalu sering seperti spaghetti logika dalam
file yang dapat dieksekusi . Setelah mengklik dua kali pada
file yang dapat dieksekusi (atau mulai dari konsol), sistem operasi memunculkan
proses .
Proses ini akan mengeksekusi
logika game , yang terdiri dari
siklus game dalam 99,42% dari waktu.
Siklus permainan memperbarui keadaan permainan sesuai dengan aturan dan
input pengguna tertentu,
menjadikan status permainan yang dihitung dalam piksel, lagi, lagi, dan lagi, dan lagi.
Siklus hidup sebuah game berjalan selama proses kompilasi:
Sebagai seorang insinyur berlebihan yang menciptakan game kompilasi keren barunya, Anda masih menggunakan bahasa favorit Anda (masih C ++, tentu saja!) Untuk menulis
logika game . Kemudian, seperti sebelumnya,
fase kompilasi berjalan, tetapi ada twist plot: Anda
menjalankan logika game Anda pada tahap kompilasi. Anda dapat menyebutnya "eksekusi" (kompilasi). Dan di sini C ++ sangat berguna; ia memiliki fitur-fitur seperti
Template Meta Programming (TMP) dan
constexpr yang memungkinkan Anda untuk melakukan
perhitungan dalam
fase kompilasi . Nanti kita akan mempertimbangkan fungsionalitas yang bisa digunakan untuk ini. Karena pada tahap ini kami menjalankan
logika permainan, maka pada saat ini kami juga perlu memasukkan
input pemain . Jelas, kompiler kami masih akan membuat
file yang dapat dieksekusi pada output. Untuk apa itu digunakan? File yang dapat dieksekusi tidak lagi berisi
loop game , tetapi memiliki misi yang sangat sederhana: menampilkan
status terhitung baru. Mari kita sebut
file yang dapat dieksekusi ini
renderer , dan
data yang dihasilkannya dibuat . Dalam
rendering kami
, efek partikel yang indah atau bayangan oklusi tidak akan terkandung, itu akan menjadi ASCII.
Render ASCII
dari keadaan terhitung baru adalah properti yang nyaman yang dapat dengan mudah ditunjukkan kepada pemain, tetapi selain itu, kami menyalinnya ke file teks. Mengapa file teks? Jelas, karena entah bagaimana dapat dikombinasikan dengan
kode dan melakukan kembali semua langkah sebelumnya, sehingga memperoleh satu
loop .
Seperti yang sudah Anda pahami, permainan yang
dijalankan selama proses kompilasi terdiri dari
siklus permainan di mana setiap
frame permainan adalah
tahap kompilasi . Setiap
tahap kompilasi menghitung
keadaan baru
dari game, yang dapat ditampilkan kepada pemain dan dimasukkan ke
frame /
tahap kompilasi berikutnya .
Anda dapat merenungkan diagram luar biasa ini sebanyak yang Anda suka hingga Anda memahami apa yang baru saja saya tulis:
Sebelum kita masuk ke detail penerapan siklus seperti itu, saya yakin Anda ingin menanyakan satu-satunya pertanyaan ...
"Kenapa repot-repot melakukan ini?"
Apakah Anda benar-benar berpikir bahwa menghancurkan idap metaprogramming C ++ saya adalah pertanyaan mendasar? Ya, tanpa hasil dalam hidup!
- Hal pertama dan terpenting adalah bahwa permainan yang dieksekusi pada tahap kompilasi akan memiliki kecepatan waktu eksekusi yang luar biasa, karena sebagian besar perhitungan dilakukan dalam fase kompilasi . Kecepatan runtime adalah kunci keberhasilan game AAA kami dengan grafis ASCII!
- Anda mengurangi kemungkinan bahwa beberapa crustacea akan muncul di repositori Anda dan meminta Anda untuk menulis ulang game di Rust . Pidato yang dipersiapkan dengan baik akan berantakan segera setelah Anda menjelaskan kepadanya bahwa pointer tidak valid tidak ada pada waktu kompilasi. Programmer Haskell yang percaya diri bahkan dapat mengonfirmasi keamanan ketik dalam kode Anda.
- Anda akan mendapatkan rasa hormat dari kerajaan hipster Javascript , di mana kerangka kerja yang dirancang ulang dengan sindrom NIH yang kuat dapat memerintah, asalkan muncul dengan nama keren.
- Seorang teman saya dulu mengatakan bahwa setiap baris kode Perl dapat digunakan secara de facto sebagai kata sandi yang sangat kuat. Saya yakin dia tidak pernah mencoba membuat kata sandi dari waktu kompilasi C ++ .
Bagaimana? Apakah Anda puas dengan jawaban saya? Maka mungkin pertanyaan Anda seharusnya: "Bagaimana Anda bisa melakukan ini?"
Sebenarnya, saya benar-benar ingin bereksperimen dengan fungsi yang ditambahkan dalam
C ++ 17 . Cukup banyak fitur yang dimaksudkan di dalamnya untuk meningkatkan efektivitas bahasa, serta untuk metaprogramming (terutama constexpr). Saya berpikir bahwa alih-alih menulis contoh kode kecil akan jauh lebih menarik untuk mengubah semua ini menjadi permainan. Proyek peliharaan adalah cara yang bagus untuk mempelajari konsep yang tidak sering Anda gunakan dalam pekerjaan Anda. Kemampuan untuk mengeksekusi logika game dasar pada waktu kompilasi kembali membuktikan bahwa template dan constepxr adalah subset
lengkap Turing dari bahasa C ++.
Ulasan Game Meta Crush Saga
Game pertandingan-3:
Meta Crush Saga adalah
gim ubin yang bergabung dengan game yang mirip dengan
Bejeweled dan
Candy Crush Saga . Inti dari aturan permainan adalah menghubungkan tiga ubin dengan pola yang sama untuk mendapatkan poin. Berikut ini sekilas
kondisi permainan yang saya “buang” (dumping di ASCII sangat mudah didapat):
R "(
Meta crush saga
------------------------
| |
| RBGBBYGR |
| |
| |
| YYGRBGBR |
| |
| |
| RBYRGRYG |
| |
| |
| RYBY (R) YGY |
| |
| |
| BGYRYGGR |
| |
| |
| RYBGYBBG |
| |
------------------------
> skor: 9009
> Bergerak: 27
) "
Gameplay dari game Match-3 ini sendiri tidak terlalu menarik, tetapi bagaimana dengan arsitektur yang semuanya bekerja? Agar Anda dapat memahaminya, saya akan mencoba menjelaskan setiap bagian dari siklus hidup game
kompilasi ini dalam hal kode.
Injeksi status permainan:
Jika Anda seorang pencinta atau pedant C ++ yang bersemangat, Anda mungkin telah memperhatikan bahwa dump kondisi permainan sebelumnya dimulai dengan pola berikut:
R "( . Sebenarnya, ini adalah
string string C ++ 11 mentah , artinya saya tidak perlu melarikan diri dari karakter khusus, misalnya,
terjemahan string : Literal string baku disimpan dalam file yang disebut
current_state.txt .
Bagaimana cara kami menyuntikkan kondisi gim saat ini ke dalam kondisi kompilasi? Mari kita tambahkan saja ke loop input!
Apakah itu file
.txt atau file
.h , arahan
sertakan dari preproses C akan bekerja dengan cara yang sama: menyalin konten file ke lokasi. Di sini saya menyalin string string literal dari status permainan di ascii ke variabel yang disebut
game_state_string .
Perhatikan bahwa
file header
loop_inputs.hpp juga memperluas input keyboard ke langkah frame / kompilasi saat ini. Berbeda dengan keadaan gim, keadaan papan ketik cukup kecil dan dapat dengan mudah diperoleh sebagai definisi preprosesor.
Menghitung status baru pada waktu kompilasi:
Sekarang kami telah mengumpulkan cukup data, kami dapat menghitung status baru. Akhirnya, kami telah mencapai titik di mana kami perlu menulis file
main.cpp :
Aneh, tetapi kode C ++ ini tidak terlihat membingungkan mengingat fungsinya. Sebagian besar kode dieksekusi dalam fase kompilasi, namun, mengikuti paradigma OOP tradisional dan pemrograman prosedural. Hanya baris terakhir - rendering - yang menjadi penghalang untuk melakukan perhitungan sepenuhnya pada waktu kompilasi. Seperti yang akan kita lihat di bawah ini, melempar sedikit constexpr di tempat yang tepat, kita bisa mendapatkan pemrograman yang cukup elegan di C ++ 17. Saya menemukan kebebasan yang menyenangkan yang diberikan C ++ kepada kami saat melakukan eksekusi campuran saat runtime dan kompilasi.
Anda juga akan melihat bahwa kode ini hanya mengeksekusi satu frame, tidak ada
loop game . Mari kita selesaikan masalah ini!
Kami merekatkan semuanya:
Jika Anda menjijikkan trik saya dengan
C ++ , maka saya harap Anda tidak keberatan melihat keterampilan
Bash saya. Faktanya,
loop game saya tidak lebih dari
script bash yang terus-menerus dikompilasi.
Sebenarnya, saya agak kesulitan mendapatkan input keyboard dari konsol. Awalnya, saya ingin mendapatkan paralel dengan kompilasi. Setelah banyak percobaan dan kesalahan, saya berhasil mendapatkan sesuatu yang kurang lebih bekerja dengan perintah
read
dari
Bash . Saya tidak pernah berani melawan
Bash penyihir dalam duel - bahasa ini terlalu menyeramkan!
Jadi, saya harus mengakui bahwa untuk mengelola siklus permainan saya harus menggunakan bahasa lain. Meskipun secara teknis tidak ada yang menghalangi saya untuk menulis bagian kode ini dalam C ++. Selain itu, ini tidak meniadakan fakta bahwa 90% dari logika permainan saya dieksekusi di dalam tim kompilasi
g ++ , yang sangat menakjubkan!
Sebuah gameplay kecil untuk membuat mata Anda beristirahat:
Sekarang setelah Anda mengalami siksaan untuk menjelaskan arsitektur gim, waktunya telah tiba untuk lukisan yang menarik:
Gif pixelated ini adalah catatan tentang bagaimana saya memainkan
Meta Crush Saga . Seperti yang Anda lihat, permainan berjalan cukup lancar untuk dapat dimainkan secara real time. Jelas, dia tidak begitu menarik sehingga saya bisa mengalirkan Twitch dan menjadi Pewdiepie baru, tetapi dia berhasil!
Salah satu aspek menyenangkan menyimpan
keadaan permainan dalam file
.txt adalah kemampuan untuk menipu atau menguji kasus-kasus ekstrem dengan sangat mudah.
Sekarang saya telah secara singkat memperkenalkan Anda pada arsitektur, kami akan mempelajari fungsionalitas C ++ 17 yang digunakan dalam proyek ini. Saya tidak akan mempertimbangkan logika game secara terperinci, karena mengacu secara eksklusif pada Match-3, tetapi sebaliknya saya akan berbicara tentang aspek C ++ yang dapat diterapkan dalam proyek lain.
Tutorial saya tentang C ++ 17:
Tidak seperti C ++ 14, yang sebagian besar berisi perbaikan kecil, standar C ++ 17 yang baru bisa memberi kita banyak hal. Ada harapan bahwa akhirnya fitur yang telah lama ditunggu-tunggu (modul, coroutine, konsep ...) akhirnya akan muncul, tetapi ... secara umum ... mereka tidak muncul; itu membuat marah banyak dari kita. Tetapi setelah menghilangkan duka, kami menemukan banyak harta tak terduga kecil yang jatuh ke standar.
Saya berani mengatakan bahwa anak-anak yang suka metaprogramming terlalu manja tahun ini! Pisahkan perubahan kecil dan tambahan pada bahasa sekarang memungkinkan Anda untuk menulis kode yang sangat berfungsi pada waktu kompilasi dan setelahnya, pada saat run time.
Constepxr di semua bidang:
Seperti yang diprediksi oleh Ben Dean dan Jason Turner dalam
laporan mereka
tentang C ++ 14 , C ++ memungkinkan Anda untuk dengan cepat meningkatkan kompilasi nilai pada waktu kompilasi dengan
constexpr kata kunci mahakuasa. Dengan menempatkan kata kunci ini di tempat yang tepat, Anda dapat memberi tahu kompiler bahwa ekspresi itu konstan dan
dapat dievaluasi langsung pada waktu kompilasi. Di
C ++ 11, kita sudah bisa menulis kode ini:
constexpr int factorial(int n)
Meskipun kata kunci
constexpr sangat kuat, ia memiliki beberapa batasan penggunaan, membuatnya sulit untuk menulis kode ekspresif dengan cara ini.
C ++ 14 telah sangat mengurangi persyaratan untuk
constexpr dan telah menjadi jauh lebih alami untuk digunakan. Fungsi faktorial kami sebelumnya dapat ditulis ulang sebagai berikut:
constexpr int factorial(int n) { if (n <= 1) { return 1; } return n * factorial(n - 1); }
C ++ 14 menyingkirkan aturan bahwa
fungsi constexpr harus terdiri dari hanya satu pernyataan kembali, yang memaksa kita untuk menggunakan
operator ternary sebagai blok pembangun utama. Sekarang
C ++ 17 membawa lebih banyak aplikasi kata kunci
constexpr yang dapat kita jelajahi!
Bercabang pada waktu kompilasi:
Apakah Anda pernah berada dalam situasi di mana Anda perlu mendapatkan perilaku yang berbeda tergantung pada parameter template yang Anda manipulasi? Misalkan kita memerlukan
serialize
function, yang akan memanggil
.serialize()
jika objek menyediakannya, jika tidak maka akan terpaksa memanggil
to_string
untuk itu. Seperti dijelaskan lebih rinci dalam
posting ini
tentang SFINAE , kemungkinan besar Anda harus menulis kode alien seperti itu:
template <class T> std::enable_if_t<has_serialize_v<T>, std::string> serialize(const T& obj) { return obj.serialize(); } template <class T> std::enable_if_t<!has_serialize_v<T>, std::string> serialize(const T& obj) { return std::to_string(obj); }
Hanya dalam mimpi Anda dapat menulis ulang
trik jelek ini
dari trik SFINAE menjadi
C ++ 14 menjadi kode yang luar biasa:
Sayangnya, ketika Anda bangun dan mulai menulis
kode C ++ 14 yang asli , kompiler Anda memuntahkan pesan yang tidak menyenangkan tentang pemanggilan
serialize(42);
. Ini menjelaskan bahwa objek bertipe
int
tidak memiliki fungsi anggota
serialize()
. Tidak peduli bagaimana itu membuat Anda marah, kompilernya benar! Dengan kode ini, ia akan selalu mencoba untuk mengkompilasi kedua cabang -
return obj.serialize();
dan
return std::to_string(obj);
. Untuk
return obj.serialize();
cabang
return obj.serialize();
Mungkin berubah menjadi semacam kode mati, karena
has_serialize(obj)
akan selalu mengembalikan
false
, tetapi kompiler masih harus mengkompilasinya.
Seperti yang mungkin Anda tebak,
C ++ 17 menyelamatkan kami dari situasi yang tidak menyenangkan, karena memungkinkan untuk menambahkan
constexpr setelah pernyataan if untuk "memaksa" bercabang pada waktu kompilasi dan membuang konstruksi yang tidak digunakan:
Jelas, ini adalah peningkatan besar
atas trik SFINAE yang harus kami terapkan sebelumnya. Setelah itu, kami mulai mendapatkan kecanduan yang sama dengan Ben dan Jason - kami mulai menggunakan
constexpr di mana-mana dan selalu. Sayangnya, ada tempat lain di mana kata kunci
constexpr cocok, tetapi belum digunakan:
parameter constexpr .
Parameter constexpr:
Jika Anda berhati-hati, Anda mungkin melihat pola aneh pada contoh kode sebelumnya. Saya berbicara tentang input loop:
Mengapa variabel
game_state_string dienkapsulasi dalam lambda constexpr? Kenapa dia tidak menjadikannya
variabel global constexpr ?
Saya ingin meneruskan variabel ini dan isinya ke beberapa fungsi. Misalnya,
Anda harus meneruskannya ke
parse_board saya dan menggunakannya dalam beberapa ekspresi konstan:
constexpr int parse_board_size(const char* game_state_string); constexpr auto parse_board(const char* game_state_string) { std::array<GemType, parse_board_size(game_state_string)> board{};
Jika kita menggunakan cara ini, kompiler grouchy akan mengeluh bahwa parameter
game_state_string bukan ekspresi konstan. Ketika saya membuat array ubin saya, saya perlu langsung menghitung kapasitas tetapnya (kita tidak bisa menggunakan vektor pada waktu kompilasi karena mereka membutuhkan alokasi memori) dan meneruskannya sebagai argumen ke template nilai di
std :: array . Oleh karena itu,
ekspresi parse_board_size (game_state_string) harus berupa ekspresi konstan. Meskipun
parse_board_size secara eksplisit ditandai sebagai
constexpr ,
game_state_string tidak dan tidak bisa! Dalam hal ini, dua aturan mengganggu kami:
- Argumen fungsi constexpr bukanlah constexpr!
- Dan kami tidak dapat menambahkan constexpr di depannya!
Semua ini bermuara pada fakta bahwa
fungsi constexpr HARUS berlaku dalam komputasi waktu runtime dan kompilasi. Dengan asumsi adanya
parameter constexpr , ini tidak akan memungkinkan mereka untuk digunakan pada saat run time.
Untungnya, ada cara untuk meratakan masalah ini. Alih-alih menerima nilai sebagai parameter fungsi biasa, kita dapat merangkum nilai ini menjadi tipe dan meneruskan tipe ini sebagai parameter templat:
template <class GameStringType> constexpr auto parse_board(GameStringType&&) { std::array<CellType, parse_board_size(GameStringType::value())> board{};
Dalam contoh kode ini, saya membuat tipe struktural GameString yang memiliki
nilai constexpr fungsi anggota statis
() yang mengembalikan string literal yang ingin saya sampaikan ke
parse_board . Di
parse_board, saya mendapatkan tipe ini melalui
parameter template
GameStringType , menggunakan aturan untuk mengekstraksi argumen template. Memiliki
GameStringType , karena fakta bahwa
value () adalah constexpr, saya cukup memanggil nilai fungsi anggota statis
() pada waktu yang tepat untuk mendapatkan string literal bahkan di tempat-tempat di mana ekspresi konstan diperlukan.
Kami berhasil merangkum literal untuk entah bagaimana meneruskannya ke
parse_board menggunakan constexpr. Namun, sangat menjengkelkan untuk perlu mendefinisikan tipe baru setiap kali Anda perlu mengirim
parse_board literal baru: "... something1 ...", "... something2 ...". Untuk mengatasi masalah ini di
C ++ 11 , Anda bisa menerapkan beberapa pengalamatan makro dan tidak langsung yang buruk menggunakan penyatuan anonim dan lambda. Michael Park menjelaskan topik ini dengan baik di
salah satu postingnya .
Di
C ++ 17, situasinya bahkan lebih baik. Jika kami mencantumkan persyaratan untuk melewatkan string literal kami, kami mendapatkan yang berikut:
- Fungsi yang dihasilkan
- Itu adalah constexpr
- Dengan nama yang unik atau anonim
Persyaratan ini harus memberi Anda petunjuk. Yang kita butuhkan adalah
constexpr lambda ! Dan di
C ++ 17, mereka secara alami menambahkan kemampuan untuk menggunakan
kata kunci constexpr untuk fungsi lambda. Kami dapat menulis ulang kode sampel kami sebagai berikut:
template <class LambdaType> constexpr auto parse_board(LambdaType&& get_game_state_string) { std::array<CellType, parse_board_size(get_game_state_string())> board{};
Percayalah, ini sudah terlihat jauh lebih nyaman daripada peretasan sebelumnya di
C ++ 11 menggunakan makro. Saya menemukan trik yang luar biasa ini berkat
Bjorn Fahler , anggota grup mitok C ++ yang saya ikuti. Baca lebih lanjut tentang trik ini di
blog -
nya . Perlu juga dipertimbangkan bahwa sebenarnya kata kunci
constexpr adalah opsional dalam hal ini: semua
lambdas dengan kemampuan untuk menjadi
constexpr akan menjadi mereka secara default. Menambahkan
constexpr secara eksplisit adalah tanda tangan yang menyederhanakan pemecahan masalah kami.
Sekarang Anda harus mengerti mengapa saya terpaksa menggunakan lambda
constexpr untuk menurunkan string yang mewakili kondisi permainan. Lihatlah fungsi lambda ini dan Anda akan memiliki pertanyaan lain lagi. Apa tipe
constexpr_string ini yang juga saya gunakan untuk membungkus stock literal?
constexpr_string dan constexpr_string_view:
Ketika bekerja dengan string, Anda seharusnya tidak memprosesnya dalam gaya C. Anda harus melupakan semua algoritma yang menjengkelkan ini yang melakukan iterasi mentah dan memeriksa nol penyelesaian! Alternatif yang ditawarkan oleh
C ++ adalah
algoritma stn :: string dan
STL yang mahakuasa. Sayangnya,
std :: string mungkin memerlukan alokasi memori pada heap (bahkan dengan Optimasi String Kecil) untuk menyimpan kontennya. Satu atau dua standar kembali, kita bisa menggunakan
constexpr baru / menghapus atau kita bisa melewati
pengalokasi constexpr ke
std :: string , tetapi sekarang kita perlu menemukan solusi lain.
Pendekatan saya adalah menulis kelas
constexpr_string dengan kapasitas tetap. Kapasitas ini diteruskan sebagai parameter ke templat nilai. Berikut ini gambaran singkat kelas saya:
template <std::size_t N>
Kelas
constexpr_string saya berusaha untuk meniru antarmuka
std :: string sedekat mungkin (untuk operasi yang saya butuhkan): kita dapat meminta
iterator dari awal dan akhir , mendapatkan
ukuran (ukuran) , mengakses
data (data) , mengakses bagian
data (data) ,
menghapus (menghapus) bagian dari mereka, dapatkan substring menggunakan substring dan sebagainya. Ini
membuatnya sangat mudah untuk mengkonversi sepotong kode dari
std :: string ke
constexpr_string . Anda mungkin bertanya-tanya apa yang terjadi ketika kita perlu menggunakan operasi yang biasanya memerlukan sorotan di
std :: string . Dalam kasus seperti itu, saya dipaksa untuk mengubahnya menjadi
operasi yang
tidak dapat
diubah yang membuat instance baru dari
constexpr_string .
Mari kita lihat operasi
append :
template <std::size_t N>
Anda tidak perlu memiliki hadiah Fields untuk mengasumsikan bahwa jika kita memiliki string ukuran
N dan string ukuran
M , maka string ukuran
N + M akan cukup untuk menyimpan penggabungan mereka. Kami mungkin membuang-buang bagian dari "repositori kompilasi-waktu", karena kedua lini mungkin tidak menggunakan seluruh kapasitas, tetapi ini adalah harga yang agak kecil untuk kenyamanan. Jelas, saya juga menulis duplikat
std :: string_view , yang disebut
constexpr_string_view .
Dengan dua kelas ini, saya siap untuk menulis kode elegan untuk menguraikan
kondisi permainan saya. Pikirkan sesuatu seperti ini:
constexpr auto game_state = constexpr_string(“...something...”);
Cukup mudah untuk beralih di sekitar permata di lapangan bermain - omong-omong, apakah Anda melihat fitur berharga lain dari
C ++ 17 dalam contoh kode ini?
Ya! Saya tidak harus secara eksplisit menentukan kapasitas
constexpr_string ketika membangunnya. Sebelumnya, ketika menggunakan
templat kelas , kami harus menunjukkan argumennya secara eksplisit. Untuk menghindari rasa sakit ini, kami membuat fungsi
make_xxx karena parameter
templat fungsi dapat dilacak. Lihat bagaimana
pelacakan argumen templat kelas mengubah kehidupan kami menjadi lebih baik:
template <int N> struct constexpr_string { constexpr_string(const char(&a)[N]) {}
Dalam beberapa situasi sulit, Anda perlu membantu kompilator untuk menghitung argumen dengan benar. Jika Anda mengalami masalah seperti itu, maka pelajari
manual untuk perhitungan argumen yang ditentukan pengguna .
Makanan gratis dari STL:
Ya, kita selalu bisa menulis ulang semuanya sendiri. Tapi mungkin anggota komite telah dengan murah hati menyiapkan sesuatu untuk kita di perpustakaan standar?
Jenis pembantu baru:
Dalam
C ++ 17 ,
std :: varian dan
std :: opsional ditambahkan ke jenis kamus standar, berdasarkan pada
constexpr . Yang pertama sangat menarik karena memungkinkan kita untuk mengekspresikan asosiasi tipe-aman, tetapi implementasi di
libstdc ++ library dengan
GCC 7.2 memiliki masalah ketika menggunakan ekspresi konstan. Oleh karena itu, saya meninggalkan ide untuk menambahkan
std :: varian ke kode saya dan hanya menggunakan
std :: opsional .
Dengan tipe T, tipe std :: opsional memungkinkan kita membuat tipe baru std :: opsional <T> , yang dapat berisi nilai tipe T atau tidak sama sekali. Ini sangat mirip dengan tipe yang bermakna yang memungkinkan nilai yang tidak ditentukan dalam C # . Mari kita lihat fungsi find_in_board , yang mengembalikan posisi elemen pertama pada bidang yang mengkonfirmasi predikat sudah benar. Mungkin tidak ada elemen seperti itu di lapangan. Untuk menangani situasi ini, jenis posisi harus opsional: template <class Predicate> constexpr std::optional<std::pair<int, int>> find_in_board(GameBoard&& g, Predicate&& p) { for (auto item : g.items()) { if (p(item)) { return {item.x, item.y}; }
Sebelumnya, kami harus menggunakan semantik pointer , atau menambahkan "keadaan kosong" langsung ke jenis posisi, atau mengembalikan boolean dan mengambil parameter output . Memang, itu cukup aneh!Beberapa tipe yang sudah ada sebelumnya juga menerima dukungan constexpr : tuple dan pair . Saya tidak akan menjelaskan secara rinci penggunaannya, karena banyak yang telah ditulis tentang mereka, tetapi saya akan membagikan salah satu kekecewaan saya. Panitia menambahkan gula sintaksis ke standar untuk mengekstraksi nilai yang terkandung dalam tuple atau pasangan . Deklarasi jenis baru ini disebut pengikat terstruktur, menggunakan tanda kurung untuk menentukan variabel mana untuk menyimpan perpecahan tuple atau pasangan : std::pair<int, int> foo() { return {42, 1337}; } auto [x, y] = foo();
Sangat pintar! Tetapi sangat disayangkan bahwa anggota komite [tidak bisa, tidak mau, tidak menemukan waktu, lupa] untuk membuat mereka ramah kepada constexpr . Saya mengharapkan sesuatu seperti ini: constexpr auto [x, y] = foo();
Sekarang kita memiliki wadah dan jenis penolong yang rumit, tetapi bagaimana kita bisa memanipulasinya?Algoritma:
Memutakhirkan wadah untuk memproses constexpr adalah tugas yang cukup monoton. Dibandingkan dengan itu, porting constexpr ke algoritma non- modifikasi tampaknya cukup sederhana. Tetapi agak aneh bahwa di C ++ 17 kami tidak melihat kemajuan di area ini, hanya akan muncul di C ++ 20 . Sebagai contoh, algoritma std :: find yang indah tidak menerima tanda tangan constexpr .Tapi jangan takut! Seperti yang dijelaskan Ben dan Jason, Anda dapat dengan mudah mengubah algoritme menjadi constexpr dengan hanya menyalin implementasi saat ini (tetapi jangan lupa tentang hak cipta); Preferensi baik. Saudara-saudara, saya hadir untuk perhatian Andaconstexpr std :: find : template<class InputIt, class T> constexpr InputIt find(InputIt first, InputIt last, const T& value) // ^ !!! constexpr. { for (; first != last; ++first) { if (*first == value) { return first; } } return last; }
Saya sudah bisa mendengar dari tribun jeritan penggemar optimasi! Ya, hanya menambahkan constexpr di depan kode sampel yang disediakan oleh cppreference mungkin tidak memberi kami kecepatan ideal saat runtime . Tetapi jika kita harus meningkatkan algoritma ini, itu akan diperlukan untuk kecepatan pada waktu kompilasi . Sejauh yang saya tahu, dalam hal kecepatan kompilasi , solusi sederhana adalah yang terbaik.Kecepatan dan bug:
Pengembang game AAA mana pun harus berinvestasi dalam memecahkan masalah ini, bukan?Kecepatan:
Ketika saya berhasil membuat versi setengah kerja dari Meta Crush Saga , pekerjaan menjadi lebih lancar. Bahkan, saya berhasil mencapai sedikit lebih dari 3 FPS (frame per detik) pada laptop lama saya dengan overclock i5 menjadi 1,80 GHz (frekuensi penting dalam kasus ini). Seperti dalam proyek apa pun, saya segera menyadari bahwa kode yang ditulis sebelumnya menjijikkan, dan mulai menulis ulang penguraian keadaan permainan menggunakan constexpr_string dan algoritma standar. Meskipun ini membuat kode jauh lebih nyaman untuk dipelihara, perubahan tersebut sangat memengaruhi kecepatan; langit-langit baru adalah 0,5 FPS .Meskipun pepatah lama tentang C ++, "abstraksi nol-kepala" tidak berlaku untuk perhitungan waktu kompilasi. Ini cukup logis jika kita menganggap kompiler sebagai penerjemah dari beberapa "kode waktu kompilasi". Perbaikan untuk berbagai kompiler masih dimungkinkan, tetapi ada juga peluang pertumbuhan bagi kami, penulis kode tersebut. Berikut adalah daftar pengamatan dan tip yang tidak lengkap yang saya temukan, mungkin khusus untuk GCC:- Array C bekerja jauh lebih baik daripada std :: array . std :: array adalah sedikit kosmetik C ++ modern di atas array gaya-C dan Anda harus membayar harga untuk menggunakannya dalam kondisi seperti itu.
- , ( ) . , , , . : , , , , ( ) , .
- , . , .
- . GCC. , «».
:
Banyak kali kompiler saya memuntahkan kesalahan kompilasi yang mengerikan, dan logika kode saya menderita. Tetapi bagaimana cara menemukan tempat di mana bug itu bersembunyi? Tanpa debugger dan printf, segalanya menjadi lebih rumit. Jika "jenggot seorang programmer" metaforis Anda belum berkembang dengan sendirinya (baik metaforis dan jenggot saya yang sebenarnya masih jauh dari harapan ini), maka Anda mungkin tidak memiliki motivasi untuk menggunakan templight atau untuk men-debug kompiler.Teman pertama kita adalah static_assert , yang memberi kita kesempatan untuk memeriksa nilai boolean waktu kompilasi. Teman kedua kami akan menjadi makro yang memungkinkan dan menonaktifkan constexpr sedapat mungkin: #define CONSTEXPR constexpr
Dengan makro ini, kita bisa membuat logika berfungsi saat runtime, yang berarti kita bisa melampirkan debugger ke dalamnya.Meta Crush Saga II - berjuang untuk gameplay sepenuhnya saat runtime:
Jelas, Meta Crush Saga tidak akan memenangkan The Game Awards tahun ini . Ini memiliki potensi besar, tetapi gameplaynya tidak sepenuhnya dieksekusi pada waktu kompilasi . Ini dapat mengganggu gamer hardcore ... Saya tidak dapat menyingkirkan skrip bash kecuali seseorang menambahkan input keyboard dan logika tidak bersih dalam fase kompilasi (dan ini adalah kegilaan yang terus terang!). Tetapi saya percaya bahwa suatu hari saya akan dapat sepenuhnya meninggalkan file yang dapat dieksekusi renderer dan menampilkan kondisi permainan pada waktu kompilasi :Orang gila dengan alias saarraz memperpanjang GCC untuk menambahkan konstruksi static_print ke bahasa . Konstruk ini harus mengambil beberapa ekspresi konstan atau string literal dan mengeluarkannya pada tahap kompilasi. Saya akan senang jika alat seperti itu ditambahkan ke standar, atau setidaknya extended static_assert sehingga menerima ekspresi konstan.Namun, dalam C ++ 17, mungkin ada cara untuk mencapai hasil ini. Compiler sudah menghasilkan dua hal - kesalahan dan peringatan ! Jika kita bisa mengelola atau mengubah peringatan sesuai kebutuhan kita, kita sudah akan menerima kesimpulan yang layak. Saya mencoba beberapa solusi, khususnyaatribut usang : template <char... words> struct useless { [[deprecated]] void call() {}
Meskipun outputnya jelas ada dan dapat diurai, sayangnya, kode tidak dapat dimainkan! Jika, secara kebetulan, Anda adalah anggota masyarakat rahasia programmer C ++ yang dapat melakukan output selama kompilasi, maka saya akan dengan senang hati mempekerjakan Anda di tim saya untuk menciptakan Meta Crush Saga II yang sempurna !Kesimpulan:
Saya akhirnya menjual Anda permainan scam saya . Saya harap Anda menemukan posting ini penasaran dan mempelajari sesuatu yang baru dalam proses membacanya. Jika Anda menemukan kesalahan atau cara untuk meningkatkan artikel, maka hubungi saya.Saya ingin berterima kasih kepada tim SwedenCpp karena mengizinkan saya melakukan laporan proyek saya di salah satu acara mereka. Selain itu, saya ingin mengucapkan terima kasih yang mendalam kepada Alexander Gurdeev , yang membantu saya meningkatkan aspek-aspek penting dari Meta Crush Saga .