Exception mengubah alur program secara mendadak, sehingga setiap fungsi yang dapat melempar exception menimbulkan pertanyaan tentang keadaan data setelah kegagalan. Konsep exception safety, yaitu keamanan exception, menjawab pertanyaan tersebut dengan tiga tingkat jaminan: basic, strong, dan nothrow. Topik ini membahas ketiganya, menunjukkan peran RAII sebagai penopang jaminan melalui guard pemulihan nilai, lalu menutupnya dengan kebiasaan yang dipilih pustaka standar.
Tiga jaminan keamanan exception#
Jaminan basic (basic guarantee) menjanjikan bahwa keadaan objek tetap konsisten dan tidak ada sumber daya yang bocor setelah exception, tanpa menentukan nilai akhirnya. Jaminan strong (strong guarantee) lebih tegas: operasi berhasil sepenuhnya atau tidak menimbulkan efek apa pun, seolah-olah operasinya belum dijalankan. Jaminan nothrow (nothrow guarantee) menjanjikan bahwa operasi tidak pernah melempar exception dalam kondisi apa pun.
Ketiga tingkat tersebut menuntut biaya yang berbeda, sehingga tidak setiap operasi perlu memberi jaminan paling kuat. Ringkasannya sebagai berikut.
| Jaminan | Yang dijanjikan | Contoh |
|---|---|---|
| Basic | invarian terjaga dan tidak ada kebocoran, nilai akhirnya tidak tertentu | sebagian besar operasi container |
| Strong | berhasil penuh atau tanpa efek sama sekali | std::vector::push_back |
| Nothrow | tidak pernah melempar exception | destructor, swap, move constructor noexcept |
Pemilihan jaminan dilakukan per operasi, bukan per program. Fungsi yang membangun ulang keadaannya dari data yang utuh cukup memberi jaminan basic, sedangkan operasi pada struktur data yang dibagikan ke banyak pemakai sebaiknya memberi jaminan strong agar keadaan lama tidak rusak di tengah jalan.
Jaminan yang lebih tinggi dapat disusun dari jaminan yang lebih rendah. Operasi dengan jaminan strong lazim dibangun dari langkah-langkah yang setidaknya basic, disimpan ke objek sementara, lalu diterapkan melalui satu operasi nothrow di bagian akhir. Penataan ini menempatkan bagian yang dapat gagal sebelum keadaan objek disentuh, sehingga titik komitnya tunggal dan mudah diidentifikasi saat kode dibaca ulang.
RAII sebagai penopang jaminan#
RAII menjamin destructor dijalankan saat stack unwinding, dan jaminan inilah yang membuat pemulihan keadaan dapat ditulis tanpa menyebarkan blok catch ke setiap langkah. Pola yang umum adalah guard, yaitu objek kecil yang mencatat keadaan lama saat dibuat dan memulihkannya saat dihancurkan, kecuali seluruh langkah sudah dinyatakan berhasil melalui pemanggilan commit. Program berikut menggunakan pola tersebut untuk mengubah nilai ambang yang divalidasi setelah penulisannya.
#include <iostream>
#include <stdexcept>
// guard RAII: memulihkan nilai lama saat dihancurkan, termasuk saat
// penghancurannya dipicu stack unwinding
class PenjagaNilai {
public:
PenjagaNilai(int& target, int nilaiBaru)
: target(target), nilaiLama(target), aktif(true) {
target = nilaiBaru;
}
// menonaktifkan pemulihan ketika seluruh langkah berhasil
void commit() { aktif = false; }
~PenjagaNilai() {
if (aktif) {
target = nilaiLama;
std::cout << "pemulihan otomatis: nilai kembali " << target << "\n";
}
}
private:
int& target;
int nilaiLama;
bool aktif;
};
// dua langkah: penulisan nilai, lalu validasi
void terapkanAmbang(int& ambang, int ambangBaru) {
PenjagaNilai jaga(ambang, ambangBaru);
if (ambangBaru < 0 || ambangBaru > 100) {
throw std::out_of_range("ambang di luar rentang 0-100");
}
jaga.commit();
}
int main() {
int ambang = 50;
terapkanAmbang(ambang, 70);
std::cout << "ambang sekarang: " << ambang << "\n";
try {
terapkanAmbang(ambang, 120);
} catch (const std::out_of_range& e) {
std::cout << "ditolak: " << e.what() << "\n";
}
std::cout << "ambang tetap: " << ambang << "\n";
return 0;
}ambang sekarang: 70
pemulihan otomatis: nilai kembali 70
ditolak: ambang di luar rentang 0-100
ambang tetap: 70Pada percobaan pertama, nilai 70 lolos validasi sehingga commit menonaktifkan pemulihan dan nilai barunya bertahan. Pada percobaan kedua, nilai 120 ditulis lebih dahulu, kemudian ditolak oleh validasi. Baris pemulihan tercetak sebelum baris ditolak karena destructor dijalankan saat exception masih menuju blok catch, dan nilai kembali ke 70, yaitu keadaan sebelum pemanggilan yang gagal. Tanpa guard, nilai 120 akan tertinggal dan invarian rentang 0-100 rusak, yang berarti jaminan basic pun tidak terpenuhi.
Bentuk guard ini menjadi fondasi pola commit or rollback, yaitu pola yang dipakai ketika satu operasi terdiri atas beberapa langkah yang sebagian dapat gagal. Selama langkah belum di-commit, setiap exception memicu pemulihan otomatis melalui destructor, sehingga penangan exception di luar fungsi cukup menangani pelaporannya.
Guard juga boleh dipasang lebih dari satu dalam satu fungsi. Objek lokal dihancurkan dengan urutan terbalik dari penciptaannya saat stack unwinding, sehingga langkah-langkah yang perlu dibatalkan dipulihkan dengan urutan yang pasti, mulai dari yang terakhir dijalankan. Sifat teraturnya inilah yang membedakan pemulihan berbasis RAII dari penanganan manual yang rentan terlewat satu langkahnya.
Jaminan pada pustaka standar#
Kebanyakan operasi pada container pustaka standar memberi jaminan basic, sedangkan sebagian operasi penting memberi jaminan strong. Operasi push_back pada std::vector memberi jaminan strong: apabila penambahan satu elemen gagal, isi vector tetap seperti semula. Jaminan ini berlaku ketika tipe elemennya dapat disalin atau move constructor-nya noexcept. Apabila keduanya tidak terpenuhi, jaminannya turun ke tingkat basic. Detail syarat seperti ini perlu diperiksa pada dokumentasi setiap operasi sebelum jaminannya diklaim.
Sementara itu, operasi yang dijanjikan tidak melempar exception membentuk kelompok kecil namun penting: destructor, swap pada container, dan move constructor yang ditandai noexcept. Operasi-operasi tersebut menjadi bahan penyusun jaminan yang lebih tinggi karena langkah yang tidak dapat gagal boleh dijalankan tanpa langkah pembatalan. Kombinasinya terlihat pada pola copy and swap, yaitu menyalin data ke objek sementara terlebih dahulu, lalu menukarnya dengan operasi yang nothrow.
Kesalahan umum#
- Mengasumsikan jaminan strong pada operasi yang hanya memberi jaminan basic. Keadaan objek setelah exception dianggap utuh, padahal isinya sudah berubah. Perbaikannya dengan membaca dokumentasi operasinya lalu menata ulang urutan langkah agar keadaan kritis terlindungi.
- Membiarkan destructor melempar exception. Stack unwinding yang memanggil destructor tersebut saat exception lain sedang berjalan berakhir di
std::terminate. Perbaikannya dengan menangkap error di dalam destructor atau memindahkan pelaporannya ke fungsi lain. - Membangun fungsi panjang tanpa titik komit yang jelas. Semakin banyak langkah di dalamnya, semakin sulit memberi jaminan strong dan keadaan setengah jadi menjadi mungkin. Perbaikannya dengan memecah langkah, menyalin data terlebih dahulu, lalu menukarnya melalui operasi yang nothrow.
Ringkasan#
- Jaminan exception bertingkat dari basic, yaitu invarian terjaga, ke strong, yaitu berhasil penuh atau tanpa efek, hingga nothrow, yaitu tidak pernah melempar exception.
- RAII menopang ketiganya karena destructor dijamin berjalan saat stack unwinding, dan pola guard dengan
commitmenyediakan pemulihan otomatis untuk operasi banyak langkah. - Pustaka standar umumnya memberi jaminan basic, memberi jaminan strong pada operasi seperti
std::vector::push_backdengan syarat tertentu, serta menandai destructor, swap, dan move noexcept sebagai operasi nothrow.
Referensi#
- Exceptions and error handling — ISO C++ FAQ — pertimbangan komunitas C++ mengenai penanganan error dan pemilihan jaminannya.
- std::vector::push_back — cppreference — syarat jaminan strong pada penambahan elemen vector.
- 14.3 — Exceptions, functions, and stack unwinding — LearnCpp — hubungan exception dengan pemanggilan destructor dan pelepasan bingkai fungsi.