
Beberapa minggu yang lalu adalah konferensi utama di dunia C ++ - CPPCON .
Lima hari berturut-turut dari jam 8 pagi sampai jam 10 malam laporan dikirim. Pemrogram dari semua agama mendiskusikan masa depan C ++, meracuni sepeda dan berpikir bagaimana membuat C ++ lebih mudah.
Anehnya banyak laporan dikhususkan untuk penanganan kesalahan. Pendekatan yang mapan tidak memungkinkan Anda untuk mencapai kinerja maksimum atau dapat menghasilkan lembar kode.
Inovasi apa yang menanti kita di C ++ 2a?
Sedikit teori
Secara konvensional, semua situasi yang salah dalam program dapat dibagi menjadi 2 kelompok besar:
- Kesalahan fatal.
- Bukan kesalahan fatal, atau yang diharapkan.
Kesalahan fatal
Setelah mereka, tidak masuk akal untuk melanjutkan eksekusi.
Misalnya, ini adalah penereferensi pointer nol, melewati memori, membaginya dengan 0, atau melanggar invarian lain dalam kode. Semua yang perlu dilakukan ketika mereka terjadi adalah untuk memberikan informasi maksimal tentang masalah dan menyelesaikan program.
Dalam c ++ terlalu banyak sudah ada cukup cara untuk menyelesaikan program:
Perpustakaan bahkan mulai muncul untuk mengumpulkan data pada crash ( 1 , 2 , 3 ).
Kesalahan tidak fatal
Ini adalah kesalahan yang disediakan oleh logika program. Misalnya, kesalahan saat bekerja dengan jaringan, mengubah string yang tidak valid ke nomor, dll. Munculnya kesalahan seperti itu dalam program ini adalah dalam urutan hal-hal. Untuk pemrosesan mereka, ada beberapa taktik yang diterima secara umum di C ++.
Kami akan membicarakannya secara lebih rinci menggunakan contoh sederhana:
Mari kita coba menulis fungsi void addTwo()
menggunakan pendekatan berbeda untuk penanganan kesalahan.
Fungsi harus membaca 2 baris, mengkonversinya menjadi int
dan mencetak jumlahnya. Perlu menangani kesalahan IO, overflow, dan konversi ke nomor. Saya akan menghilangkan detail implementasi yang tidak menarik. Kami akan mempertimbangkan 3 pendekatan utama.
1. Pengecualian
// // IO std::runtime_error std::string readLine(); // int // std::invalid_argument int parseInt(const std::string& str); // a b // std::overflow_error int safeAdd(int a, int b); void addTwo() { try { std::string aStr = readLine(); std::string bStr = readLine(); int a = parseInt(aStr); int b = parseInt(bStr); std::cout << safeAdd(a, b) << std::endl; } catch(const std::exeption& e) { std::cout << e.what() << std::endl; } }
Pengecualian dalam C ++ memungkinkan Anda untuk menangani kesalahan secara terpusat tanpa
tidak perlu
,
tetapi Anda harus membayar untuk ini dengan banyak masalah.
- overhead yang terlibat dalam penanganan pengecualian cukup besar, Anda tidak bisa sering melempar pengecualian.
- lebih baik untuk tidak membuang pengecualian dari konstruktor / destruktor dan untuk mengamati RAII.
- oleh tanda tangan fungsi, tidak mungkin untuk memahami pengecualian apa yang bisa terbang keluar dari fungsi.
- ukuran file biner meningkat karena kode dukungan pengecualian tambahan.
2. Kode pengembalian
Pendekatan klasik yang diwarisi dari C.
bool readLine(std::string& str); bool parseInt(const std::string& str, int& result); bool safeAdd(int a, int b, int& result); void processError(); void addTwo() { std::string aStr; int ok = readLine(aStr); if (!ok) { processError(); return; } std::string bStr; ok = readLine(bStr); if (!ok) { processError(); return; } int a = 0; ok = parseInt(aStr, a); if (!ok) { processError(); return; } int b = 0; ok = parseInt(bStr, b); if (!ok) { processError(); return; } int result = 0; ok = safeAdd(a, b, result); if (!ok) { processError(); return; } std::cout << result << std::endl; }
Tidak terlihat sangat bagus?
- Anda tidak dapat mengembalikan nilai sebenarnya dari suatu fungsi.
- Sangat mudah untuk melupakan menangani kesalahan (terakhir kali Anda memeriksa kode pengembalian dari printf?).
- Anda harus menulis kode penanganan kesalahan di sebelah setiap fungsi. Kode seperti itu lebih sulit dibaca.
Menggunakan C ++ 17 dan C ++ 2a akan memperbaiki semua masalah ini secara berurutan.
3. C ++ 17 dan mengangguk
nodiscard
di C ++ 17.
Jika Anda menentukannya sebelum deklarasi fungsi, maka ketiadaan memeriksa nilai kembali akan menyebabkan peringatan kompiler.
[[nodiscard]] bool doStuff(); doStuff();
Anda juga dapat menentukan nodiscard
untuk kelas, struktur, atau kelas enum.
Dalam hal ini, tindakan atribut meluas ke semua fungsi yang mengembalikan nilai dari tipe berlabel nodiscard
.
enum class [[nodiscard]] ErrorCode { Exists, PermissionDenied }; ErrorCode createDir(); /* ... */ createDir();
Saya tidak akan memberikan kode dengan nodiscard
.
C ++ 17 std :: opsional
Di C ++ 17, std::optional<T>
.
Mari kita lihat bagaimana kode itu terlihat sekarang.
std::optional<std::string> readLine(); std::optional<int> parseInt(const std::string& str); std::optional<int> safeAdd(int a, int b); void addTwo() { std::optional<std::string> aStr = readLine(); std::optional<std::string> bStr = readLine(); if (aStr == std::nullopt || bStr == std::nullopt){ std::cerr << "Some input error" << std::endl; return; } std::optional<int> a = parseInt(*aStr); std::optional<int> b = parseInt(*bStr); if (!a || !b) { std::cerr << "Some parse error" << std::endl; return; } std::optional<int> result = safeAdd(*a, *b); if (!result) { std::cerr << "Integer overflow" << std::endl; return; } std::cout << *result << std::endl; }
Anda dapat menghapus argumen masuk-keluar dari fungsi dan kode akan menjadi lebih bersih.
Namun, kami kehilangan informasi kesalahan. Tidak jelas kapan dan apa yang salah.
Anda dapat mengganti std::optional
dengan std::variant<ResultType, ValueType>
.
Arti kode sama dengan std::optional
, tetapi lebih rumit.
C ++ 2a dan std :: diharapkan
std::expected<ResultType, ErrorType>
- jenis template khusus , mungkin akan jatuh ke standar terdekat yang tidak lengkap.
Ini memiliki 2 parameter.
Bagaimana ini berbeda dari variant
biasa? Apa yang membuatnya istimewa?
std::expected
akan menjadi monad .
Diusulkan untuk mendukung banyak operasi di std::expected
seperti pada monad: map
, catch_error
, bind
, catch_error
, return
dan then
.
Dengan menggunakan fungsi-fungsi ini, Anda dapat mengaitkan panggilan fungsi menjadi sebuah rantai.
getInt().map([](int i)return i * 2;) .map(integer_divide_by_2) .catch_error([](auto e) return 0; );
Misalkan kita memiliki fungsi dengan mengembalikan std::expected
.
std::expected<std::string, std::runtime_error> readLine(); std::expected<int, std::runtime_error> parseInt(const std::string& str); std::expected<int, std::runtime_error> safeAdd(int a, int b);
Di bawah ini hanya pseudo-code, tidak dapat dipaksa untuk bekerja di kompiler modern mana pun.
Anda dapat mencoba meminjam dari Haskell do-sintaks untuk merekam operasi pada monads. Mengapa tidak mengizinkannya melakukannya:
std::expected<int, std::runtime_error> result = do { auto aStr <- readLine(); auto bStr <- readLine(); auto a <- parseInt(aStr); auto b <- parseInt(bStr); return safeAdd(a, b) }
Beberapa penulis menyarankan sintaks ini:
try { auto aStr = try readLine(); auto bStr = try readLine(); auto a = try parseInt(aStr); auto b = try parseInt(bStr); std::cout result << std::endl; return safeAdd(a, b) } catch (const std::runtime_error& err) { std::cerr << err.what() << std::endl; return 0; }
Kompiler secara otomatis mengubah blok kode tersebut menjadi urutan panggilan fungsi. Jika pada suatu titik fungsi kembali tidak seperti yang diharapkan, rantai perhitungan akan terputus. Ya, dan sebagai jenis kesalahan, Anda dapat menggunakan jenis pengecualian yang sudah ada dalam standar: std::runtime_error
, std::out_of_range
, dll.
Jika Anda dapat mendesain sintaks dengan baik, maka std::expected
akan memungkinkan Anda untuk menulis kode sederhana dan efisien.
Kesimpulan
Tidak ada cara ideal untuk menangani kesalahan. Sampai saat ini, di C ++ hampir ada semua kemungkinan metode penanganan kesalahan kecuali monad.
Dalam C ++ 2a, semua metode yang mungkin cenderung muncul.
Apa yang harus dibaca dan dilihat pada topik
- Proposal aktual .
- Pidato tentang std :: diharapkan dengan CPPCON .
- Andrei Alexandrescu tentang std :: diharapkan di C ++ Rusia .
- Kurang lebih diskusi tentang proposal Reddit baru-baru ini .