
Di C ++ 20, pemrograman kontrak telah muncul. Sampai saat ini, belum ada kompiler yang mengimplementasikan dukungan untuk fitur ini.
Tetapi ada cara sekarang untuk mencoba menggunakan kontrak dari C ++ 20, seperti yang dijelaskan dalam standar.
TL; DR
Ada dentang garpu yang mendukung kontrak. Menggunakan contohnya, saya memberi tahu Anda cara menggunakan kontrak sehingga segera setelah fitur muncul di kompiler favorit Anda, Anda dapat segera mulai menggunakannya.
Sudah banyak yang ditulis tentang pemrograman kontrak, tetapi singkatnya saya akan memberi tahu Anda apa itu dan untuk apa.
Logika Hoar
Paradigma kontrak didasarkan pada logika Hoar ( 1 , 2 ).
Logika hoar adalah cara untuk secara formal membuktikan kebenaran suatu algoritma.
Ini beroperasi dengan konsep-konsep seperti prasyarat, pascakondisi dan invarian.
Dari sudut pandang praktis, penggunaan logika Hoar adalah, pertama, cara membuktikan secara formal kebenaran suatu program dalam kasus di mana kesalahan dapat menyebabkan bencana atau hilangnya nyawa. Kedua, cara untuk meningkatkan keandalan program, bersama dengan analisis dan pengujian statis.
Pemrograman kontrak
( 1 , 2 )
Gagasan utama kontrak adalah bahwa, dengan analogi dengan kontrak dalam bisnis, perjanjian dijelaskan untuk setiap fungsi atau metode. Pengaturan ini harus diperhatikan oleh penelepon dan penelepon.
Bagian integral dari kontrak adalah setidaknya dua mode perakitan - debugging dan toko bahan makanan. Kontrak harus berperilaku berbeda tergantung pada mode build. Praktik yang paling umum adalah memeriksa kontrak di unit debug dan mengabaikannya di toko.
Kadang-kadang kontrak juga diperiksa dalam rakitan produk dan ketidakpatuhannya dapat, misalnya, mengarah pada pembentukan pengecualian.
Perbedaan utama antara penggunaan kontrak dari pendekatan "klasik" adalah bahwa penelepon harus mematuhi prasyarat callee, yang dijelaskan dalam kontrak, dan penelepon harus mematuhi postconditions dan invariannya.
Oleh karena itu, pihak yang dipanggil tidak diharuskan memverifikasi kebenaran parameternya. Kewajiban ini ditugaskan untuk penelepon oleh kontrak.
Ketidakpatuhan terhadap kontrak harus dideteksi pada tahap pengujian dan melengkapi semua jenis pengujian: integrasi modular, dll.
Sekilas, penggunaan kontrak membuat pengembangan lebih sulit dan menurunkan keterbacaan kode. Faktanya, kebalikannya adalah benar. Penganut pengetikan statis akan merasa lebih mudah untuk mengevaluasi manfaat dari kontrak, karena opsi termudah mereka adalah untuk menggambarkan jenis dalam tanda tangan metode dan fungsi.
Jadi, apa manfaat dari kontrak:
- Tingkatkan keterbacaan kode melalui dokumentasi eksplisit.
- Tingkatkan keandalan kode dengan melengkapi pengujian.
- Izinkan kompiler menggunakan optimasi tingkat rendah dan menghasilkan kode lebih cepat berdasarkan kepatuhan kontrak. Dalam kasus yang terakhir, ketidakpatuhan terhadap kontrak dalam majelis rilis dapat menyebabkan UB.
Pemrograman kontrak dalam C ++
Pemrograman kontrak diimplementasikan dalam banyak bahasa. Contoh yang paling mencolok adalah Eiffel , di mana paradigma pertama kali diterapkan, dan D , dalam kontrak D adalah bagian dari bahasa tersebut.
Dalam C ++, sebelum standar C ++ 20, kontrak dapat digunakan sebagai pustaka yang terpisah.
Pendekatan ini memiliki beberapa kelemahan:
- Sintaksnya sangat kikuk menggunakan macro.
- Kurangnya gaya tunggal.
- Ketidakmampuan untuk menggunakan kontrak oleh kompiler untuk mengoptimalkan kode.
Implementasi perpustakaan biasanya didasarkan pada penggunaan arahan lama dan preprosesor yang baik yang memeriksa flag kompilasi.
Menggunakan kontrak dalam formulir ini benar-benar membuat kode jelek dan tidak dapat dibaca. Ini adalah salah satu alasan mengapa menggunakan kontrak dalam C ++ sedikit dipraktikkan.
Ke depan, saya akan menunjukkan bagaimana penggunaan kontrak akan terlihat dalam C ++ 20.
Dan kemudian, kami akan menganalisis semua ini secara lebih rinci:
int f(int x, int y) [[ expects: x > 0 ]]
Coba
Sayangnya, saat ini, tidak ada kompiler yang banyak digunakan belum menerapkan dukungan kontrak.
Tapi ada jalan keluar.
Kelompok penelitian ARCOS dari Universidad Carlos III de Madrid menerapkan dukungan eksperimental untuk kontrak di cabang clang ++.
Agar tidak "menulis kode pada selembar kertas", tetapi untuk dapat segera mencoba peluang baru dalam bisnis, kami dapat mengumpulkan garpu ini dan menggunakannya untuk mencoba contoh di bawah ini.
Instruksi perakitan dijelaskan dalam readme dari repositori github
https://github.com/arcosuc3m/clang-contracts
git clone https://github.com/arcosuc3m/clang-contracts/ mkdir -p clang-contracts/build/ && cd clang-contracts/build/ cmake -G "Unix Makefiles" -DLLVM_USE_LINKER=gold -DBUILD_SHARED_LIBS=ON -DLLVM_USE_SPLIT_DWARF=ON -DLLVM_OPTIMIZED_TABLEGEN=ON ../ make -j8
Saya tidak punya masalah selama perakitan, tetapi mengkompilasi sumber membutuhkan waktu yang sangat lama.
Untuk mengkompilasi contoh-contoh, Anda harus secara eksplisit menentukan jalur ke biner dentang ++.
Sebagai contoh, ini terlihat seperti ini untuk saya
/home/valmat/work/git/clang-contracts/build/bin/clang++ -std=c++2a -build-level=audit -g test.cpp -o test.bin
Saya telah menyiapkan contoh untuk memudahkan Anda memeriksa kontrak menggunakan contoh kode nyata. Saya sarankan, sebelum Anda mulai membaca bagian selanjutnya, klon dan kompilasi contoh.
git clone https://github.com/valmat/cpp20-contracts-examples/ cd cpp20-contracts-examples make CPP=/path/to/clang++
Di sini /path/to/clang++
path ke clang++
binary dari rakitan kompiler eksperimental Anda.
Selain kompiler itu sendiri, grup riset ARCOS menyiapkan versi mereka dari Compiler Explorer untuk fork mereka.
Pemrograman kontrak dalam C ++ 20
Sekarang tidak ada yang menghalangi kita untuk mulai meneliti kemungkinan yang ditawarkan oleh pemrograman kontrak, dan segera mencoba peluang ini dalam praktik.
Seperti disebutkan di atas, kontrak dibangun dari prasyarat, postkondisi dan invarian (pernyataan).
Di C ++ 20, atribut dengan sintaks berikut digunakan untuk ini
[[contract-attribute modifier identifier: conditional-expression]]
Di mana contract-attribute
dapat mengambil salah satu dari nilai berikut:
mengharapkan , memastikan atau menegaskan .
expects
digunakan untuk prasyarat, ensures
pascakondisi dan pernyataan pernyataan.
conditional-expression
adalah conditional-expression
Boolean yang divalidasi dalam predikat kontrak.
modifier
dan identifier
dapat dihilangkan.
Mengapa saya memerlukan modifier
saya akan menulis sedikit lebih rendah.
identifier
hanya digunakan dengan ensures
dan digunakan untuk mewakili nilai kembali.
Prasyarat memiliki akses ke argumen.
Postconditions memiliki akses ke nilai yang dikembalikan oleh fungsi. Sintaks digunakan untuk ini.
[[ensures return_variable: expr(return_variable)]]
Di mana return_variable
ekspresi apa pun yang valid untuk variabel.
Dengan kata lain, prasyarat dimaksudkan untuk menyatakan pembatasan yang dikenakan pada argumen yang diterima oleh fungsi, dan postkondisi untuk menyatakan pembatasan yang dikenakan pada nilai yang dikembalikan oleh fungsi.
Dipercayai bahwa prasyarat dan postkondisi adalah bagian dari antarmuka fungsi, sedangkan pernyataan adalah bagian dari implementasinya.
Predikat prakondisi selalu dievaluasi segera sebelum fungsi dijalankan. Kondisi pos terpenuhi segera setelah fungsi kontrol beralih ke kode panggilan.
Jika pengecualian dilemparkan dalam suatu fungsi, maka kondisi akhir tidak akan diperiksa.
Kondisi akhir diperiksa hanya jika fungsi selesai secara normal.
Jika pengecualian terjadi saat memeriksa ekspresi dalam kontrak, std::terminate()
akan dipanggil.
Prasyarat dan postkondisi selalu dijelaskan di luar fungsi tubuh dan tidak dapat memiliki akses ke variabel lokal.
Jika prasyarat dan postkondisi menggambarkan kontrak untuk metode kelas publik, mereka tidak dapat memiliki akses ke bidang kelas pribadi dan yang dilindungi. Jika metode kelas dilindungi, maka ada akses ke data publik yang dilindungi dan publik, tetapi tidak untuk pribadi.
Batasan terakhir sepenuhnya logis, mengingat bahwa kontrak adalah bagian dari antarmuka metode.
Pernyataan (invarian) selalu dijelaskan dalam tubuh fungsi atau metode. Secara desain, mereka adalah bagian dari implementasi. Dan, karenanya, mereka dapat memiliki akses ke semua data yang tersedia. Termasuk variabel fungsi lokal dan bidang kelas pribadi dan yang dilindungi.
contoh 1
Kami mendefinisikan dua prasyarat, satu pascakondisi dan satu invarian:
int foo(int x, int y) [[ expects: x > y ]]
contoh 2
Prasyarat metode publik tidak dapat merujuk ke bidang yang dilindungi atau pribadi:
struct X {
Modifikasi variabel dalam ekspresi yang dijelaskan oleh atribut kontrak tidak diperbolehkan. Jika rusak, akan ada UB.
Ekspresi yang dijelaskan dalam kontrak tidak boleh memiliki efek samping. Meskipun kompiler dapat memverifikasi ini, mereka tidak diharuskan untuk melakukannya. Pelanggaran terhadap persyaratan ini dianggap sebagai perilaku yang tidak terdefinisi.
struct X { int m = 5; int foo(int n) [[ expects: n < m++ ]]
Persyaratan untuk tidak mengubah keadaan program dalam ekspresi kontrak akan menjadi sedikit lebih rendah ketika saya berbicara tentang tingkat pengubah kontrak dan mode pembuatan.
Sekarang saya hanya mencatat bahwa program yang benar harus berfungsi seperti tidak ada kontrak sama sekali.
Seperti yang saya sebutkan di atas, dalam kontrak Anda dapat menentukan sebanyak mungkin prasyarat dan kondisi akhir yang Anda inginkan.
Semuanya akan diperiksa secara berurutan. Tetapi prasyarat selalu diperiksa sebelum fungsi dijalankan, dan postkondisi segera setelah keluar.
Ini berarti bahwa prasyarat selalu diperiksa terlebih dahulu, seperti diilustrasikan dalam contoh berikut:
int foo(int n) [[ expects: expr(n) ]]
Ekspresi di postconditions dapat merujuk tidak hanya pada nilai yang dikembalikan oleh fungsi, tetapi juga pada argumen fungsi.
int foo(int &n) [[ ensures: expr(n) ]];
Dalam hal ini, Anda dapat menghilangkan pengidentifikasi nilai pengembalian.
Jika postcondition merujuk pada argumen fungsi, maka argumen ini dianggap pada titik keluar dari fungsi , dan bukan pada titik masuk, seperti halnya dengan prasyarat.
Tidak ada cara untuk merujuk ke nilai asli (pada titik masuk fungsi) di postcondition.
contoh :
void incr(int &n) [[ expects: 3 == n ]] [[ ensures: 4 == n ]] {++n;}
Predikat dalam kontrak dapat merujuk ke variabel lokal hanya jika masa pakai dari variabel-variabel ini sesuai dengan waktu perhitungan predikat.
Misalnya, untuk fungsi constexpr
, variabel lokal tidak dapat dirujuk kecuali mereka diketahui pada waktu kompilasi.
contoh :
int a = 1; constexpr int b = 100; constexpr int foo(int n) [[ expects: a <= n ]]
Kontrak untuk pointer fungsi
Anda tidak dapat menentukan kontrak untuk pointer fungsi, tetapi Anda dapat menetapkan alamat fungsi yang kontraknya didefinisikan untuk pointer fungsi.
contoh :
int foo(int n) [[expects: n < 10]] { return n*n; } int (*pfoo)(int n) = &foo;
Memanggil pfoo(100)
akan melanggar kontrak.
Kontrak Warisan
Implementasi klasik dari konsep kontrak menunjukkan bahwa prasyarat dapat dilemahkan dalam subkelas, postkondisi dan invarian dapat diperkuat dalam subkelas.
Dalam implementasi C ++ 20, ini tidak terjadi.
Pertama, invarian di C ++ 20 adalah bagian dari implementasi, bukan antarmuka. Karena alasan ini, mereka dapat diperkuat dan dilemahkan. Jika tidak ada assert
dalam implementasi fungsi virtual, maka itu tidak akan diwarisi.
Kedua, diperlukan bahwa ketika mewarisi fungsi identik ODR .
Dan, karena prasyarat dan postkondisi adalah bagian dari antarmuka, maka dalam pewarisnya mereka harus sama persis.
Selain itu, deskripsi prasyarat dan postkondisi selama pewarisan dapat dihilangkan. Tetapi jika mereka dideklarasikan, maka mereka harus benar-benar cocok dengan definisi di kelas dasar.
contoh :
struct Base { virtual int foo(int n) [[ expects: n < 10 ]] [[ ensures r: r > 100 ]] { return n*n; } }; struct Derived1 : Base { virtual int foo(int n) override [[ expects: n < 10 ]] [[ ensures r: r > 100 ]] { return n*n*2; } }; struct Derived2 : Base {
KomentarSayangnya, contoh di atas tidak berfungsi di kompiler eksperimental seperti yang diharapkan.
Jika foo
dari Derived2
kontrak, maka itu tidak akan diwarisi dari kelas dasar. Selain itu, kompiler memungkinkan Anda untuk menentukan subkelas kontrak yang tidak cocok dengan kontrak dasar.
Kesalahan kompilator eksperimental lain:
catatan harus benar secara sintaksis
virtual int foo(int n) override [[expects: n < 10]] {...}
Namun, dalam formulir ini, saya menerima kesalahan kompilasi
inheritance1.cpp:20:36: error: expected ';' at end of declaration list virtual int foo(int n) override ^ ;
dan harus diganti oleh
virtual int foo(int n) [[expects: n < 10]] override {...}
Saya pikir ini disebabkan oleh kekhasan kompiler eksperimental, dan kode yang benar sintaks akan bekerja di versi rilis kompiler.
Pengubah kontrak
Cek predikat kontrak dapat menimbulkan biaya pemrosesan tambahan.
Oleh karena itu, praktik umum adalah memeriksa kontrak dalam pengembangan dan menguji build dan mengabaikannya dalam rilis build.
Untuk tujuan ini, standar menawarkan tiga tingkat pengubah kontrak. Menggunakan pengubah dan kunci kompiler, programmer dapat mengontrol kontak mana yang diperiksa dalam perakitan dan yang diabaikan.
default
- pengubah ini digunakan secara default. Diasumsikan bahwa biaya komputasi untuk memverifikasi eksekusi ekspresi dengan pengubah ini kecil dibandingkan dengan biaya komputasi fungsi itu sendiri.audit
- pengubah ini mengasumsikan bahwa biaya komputasi untuk memverifikasi eksekusi suatu ekspresi adalah signifikan dibandingkan dengan biaya komputasi fungsi itu sendiri.axiom
- pengubah ini digunakan jika ekspresi adalah deklaratif. Tidak dicentang saat runtime. Berfungsi untuk mendokumentasikan antarmuka suatu fungsi, untuk digunakan oleh analisis statis dan pengoptimal kompiler. Ekspresi dengan pengubah axiom
pernah dievaluasi saat runtime.
Contoh
[[expects: expr]]
Dengan menggunakan pengubah, Anda dapat menentukan cek mana versi rakitan Anda akan digunakan dan yang akan dinonaktifkan.
Perlu dicatat bahwa meskipun pemeriksaan tidak dilakukan, kompiler memiliki hak untuk menggunakan kontrak untuk optimasi tingkat rendah. Meskipun verifikasi kontrak dapat dinonaktifkan oleh flag kompilasi, pelanggaran kontrak menyebabkan perilaku program yang tidak ditentukan.
Atas kebijakan kompiler, fasilitas dapat disediakan untuk memungkinkan axiom
ekspresi yang ditandai sebagai axiom
.
Dalam kasus kami, ini adalah opsi kompiler
-axiom-mode=<mode>
-axiom-mode=on
aksioma dan, karenanya, mematikan verifikasi klaim dengan axiom
pengidentifikasi,
-axiom-mode=off
mematikan mode aksioma dan, karenanya, memungkinkan verifikasi pernyataan dengan axiom
pengidentifikasi.
contoh :
int foo(int n) [[expects axiom: n < 10]] { return n*n; }
Suatu program dapat dikompilasi dengan tiga tingkat verifikasi yang berbeda:
off
mematikan semua pemeriksaan ekspresi dalam kontrak- ekspresi hanya
default
dengan pengubah default
diperiksa audit
mode lanjutan saat semua pemeriksaan dilakukan dengan pengubah default
dan audit
Bagaimana tepatnya menerapkan instalasi tingkat verifikasi adalah pada kebijaksanaan pengembang kompiler.
Dalam kasus kami, opsi kompiler digunakan untuk ini
-build-level=<off|default|audit>
Standarnya adalah -build-level=default
Seperti yang telah disebutkan, kompiler dapat menggunakan kontrak untuk optimasi tingkat rendah. Karena alasan ini, terlepas dari kenyataan bahwa pada saat pelaksanaan beberapa predikat dalam kontrak (tergantung pada tingkat verifikasi) mungkin tidak dihitung, ketidakpatuhannya mengarah pada perilaku yang tidak terdefinisi.
Saya akan menunda contoh penerapan level perakitan hingga bagian berikutnya, di mana mereka dapat dibuat visual.
Intersepsi pelanggaran kontrak
Bergantung pada opsi apa yang akan dilakukan oleh program, jika terjadi pelanggaran kontrak mungkin ada skenario perilaku yang berbeda.
Secara default, pelanggaran kontrak menyebabkan program mogok, memanggil std::terminate()
. Tetapi programmer dapat mengesampingkan perilaku ini dengan menyediakan penangannya sendiri dan menunjukkan kepada kompiler bahwa perlu untuk melanjutkan program setelah pelanggaran kontrak.
Pada kompilasi, Anda dapat menginstal handler pelanggaran , yang disebut ketika kontrak dilanggar.
Cara untuk mengimplementasikan instalasi pawang adalah atas kebijaksanaan pembuat kompiler.
Dalam kasus kami, ini
-contract-violation-handler=<violation_handler>
Tanda tangan prosesor seharusnya
void(const std::contract_violation& info)
atau
void(const std::contract_violation& info) noexcept
std::contract_violation
setara dengan definisi berikut:
struct contract_violation { uint_least32_t line_number() const noexcept; std::string_view file_name() const noexcept; std::string_view function_name() const noexcept; std::string_view comment() const noexcept; std::string_view assertion_level() const noexcept; };
Dengan demikian, pawang memungkinkan Anda untuk mendapatkan informasi yang cukup komprehensif tentang di mana dan dalam kondisi apa pelanggaran kontrak terjadi.
Jika handler handler pelanggaran ditentukan, maka dalam kasus pelanggaran kontrak, secara default, std::abort()
akan dipanggil segera setelah eksekusi (tanpa menentukan handler, std::terminate()
akan dipanggil).
Standar ini mengasumsikan bahwa kompiler menyediakan alat yang memungkinkan pemrogram untuk terus menjalankan suatu program setelah pelanggaran kontrak.
Cara untuk mengimplementasikan alat-alat ini diserahkan kepada kebijaksanaan pengembang kompiler.
Dalam kasus kami, ini adalah opsi kompiler
-fcontinue-after-violation
Opsi -fcontinue-after-violation
dan -contract-violation-handler
dapat diatur secara independen satu sama lain. Misalnya, Anda dapat mengatur -fcontinue-after-violation
, tetapi tidak mengatur -contract-violation-handler
. Dalam kasus terakhir, setelah pelanggaran kontrak, program hanya akan terus bekerja.
Kemampuan untuk melanjutkan program setelah pelanggaran kontrak ditentukan oleh standar, tetapi harus diperhatikan dengan fitur ini.
Secara teknis, perilaku suatu program setelah pelanggaran kontrak tidak didefinisikan, bahkan jika programmer secara eksplisit menunjukkan bahwa program tersebut harus terus bekerja.
Ini karena kompiler dapat melakukan optimasi tingkat rendah berdasarkan pada eksekusi kontrak.
Idealnya, jika terjadi pelanggaran kontrak, Anda perlu mencatat informasi diagnostik sesegera mungkin dan menghentikan program. Anda perlu memahami apa yang Anda lakukan dengan membiarkan program bekerja setelah pelanggaran.
Tentukan pawang Anda dan gunakan untuk mencegat pelanggaran kontrak
void violation_handler(const std::contract_violation& info) { std::cerr << "line_number : " << info.line_number() << std::endl; std::cerr << "file_name : " << info.file_name() << std::endl; std::cerr << "function_name : " << info.function_name() << std::endl; std::cerr << "comment : " << info.comment() << std::endl; std::cerr << "assertion_level : " << info.assertion_level() << std::endl; }
Dan pertimbangkan contoh pelanggaran kontrak:
#include "violation_handler.h" int foo(int n) [[expects: n < 10]] { return n*n; } int main() { foo(100);
Kami mengkompilasi program dengan opsi -contract-violation-handler=violation_handler
dan -fcontinue-after-violation
dan jalankan
$ bin/example8-handling.bin line_number : 4 file_name : example8-handling.cpp function_name : foo comment : n < 10 assertion_level : default
Sekarang kita dapat memberikan contoh yang menunjukkan perilaku program jika terjadi pelanggaran kontrak di berbagai tingkat perakitan dan mode kontrak.
Perhatikan contoh berikut:
#include "violation_handler.h" int foo(int n) [[ expects axiom : n < 100 ]] [[ expects default : n < 200 ]] [[ expects audit : n < 300 ]] { return 2 * n; } int main() { foo(350);
Jika Anda membangunnya dengan opsi -build-level=off
, maka seperti yang diharapkan, kontrak tidak akan diperiksa.
Berkumpul dengan level default
(dengan opsi -build-level=default
), kita mendapatkan output berikut:
$ bin/example9-default.bin line_number : 5 file_name : example9.cpp function_name : foo comment : n < 200 assertion_level : default line_number : 5 file_name : example9.cpp function_name : foo comment : n < 200 assertion_level : default
Dan majelis dengan tingkat audit
akan memberikan:
$ bin/example9-audit.bin line_number : 5 file_name : example9.cpp function_name : foo comment : n < 200 assertion_level : default line_number : 6 file_name : example9.cpp function_name : foo comment : n < 300 assertion_level : audit line_number : 5 file_name : example9.cpp function_name : foo comment : n < 200 assertion_level : default
Komentar
violation_handler
dapat melempar pengecualian. Dalam hal ini, Anda dapat mengonfigurasi program sehingga pelanggaran kontrak menyebabkan pelemparan pengecualian.
Jika fungsi yang dijelaskan kontrak ditandai sebagai noexcept
dan ketika memeriksa kontrak violation_handler
dipanggil, yang melempar pengecualian, maka std::terminate()
akan dipanggil.
Contoh
void violation_handler(const std::contract_violation&) { throw std::exception(); } int foo(int n) noexcept [[ expects: n > 0 ]] { return n*n; } int main() { foo(0);
Jika flag diteruskan ke kompiler: jangan melanjutkan menjalankan program setelah melanggar kontrak ( continuation mode=off
), tetapi penangan pelanggaran melempar pengecualian, maka std::terminate()
akan dipaksa.
Kesimpulan
Kontrak terkait dengan pemeriksaan runtime non-intrusif. Mereka memainkan peran yang sangat penting dalam memastikan kualitas perangkat lunak yang dirilis.
C ++ digunakan sangat luas. Dan pasti akan ada jumlah klaim yang cukup untuk spesifikasi kontrak. Menurut pendapat subjektif saya, implementasinya ternyata cukup nyaman dan visual.
Kontrak C ++ 20 akan membuat program kami lebih andal, cepat, dan mudah dipahami. Saya menantikan implementasinya dalam kompiler.
PS
Dalam PM, mereka memberi tahu saya bahwa mungkin dalam versi final dari standar expects
dan ensures
akan digantikan oleh masing-masing pre
dan post
.