Melempar exception saat sebuah function sedang mengubah data menimbulkan pertanyaan yang tidak ada pada kode tanpa exception: apa keadaan objek itu SETELAH kegagalan? Jika push_back gagal di tengah jalan, apakah vector tetap utuh? Jika operasi gagal setelah separuh field diperbarui, apakah objek berada dalam keadaan yang bisa dipercaya? Pertanyaan-pertanyaan ini punya jawaban formal berupa tiga tingkat jaminan yang dijanjikan pustaka standar — dan yang wajib kita janjikan sendiri pada class yang kita tulis. Chapter ini merumuskannya, membuktikan bedanya lewat dua implementasi operasi yang sama, lalu menghubungkannya dengan RAII yang sudah kita kuasai.

Tiga Tingkat Jaminan #

Pustaka standar dan literatur C++ merumuskan tiga tingkat, dari yang paling longgar ke paling ketat:

JaminanJanji yang diberikanContoh
BasicInvariant tetap terjaga: tidak ada kebocoran, tidak ada data rusak; objek masih valid, destructible, dan bisa diassign/destroy — tetapi nilai akhirnya tidak ditentukanstd::vector::insert saat elemen baru melempar
StrongTransaksi penuh: operasi berhasil utuh atau tidak berefek sama sekali — state objek persis seperti sebelum dipanggilstd::vector::push_back untuk tipe yang move-nya noexcept
noexceptTidak pernah melempar sama sekali; operasi selalu berhasilDestructor, std::swap, move constructor yang noexcept

Jaminan basic adalah batas bawah yang wajib untuk semua kode: tanpa itu, kegagalan bisa meninggalkan pointer menggantung atau invariant yang rusak, dan bagian lain program bisa jebol saat menyentuh objek yang “setengah jadi” itu. Jaminan strong menambahkan kemudahan berpikir gaya transaksi — pemanggil bisa berasumsi gagal berarti tidak terjadi apa-apa. Jaminan noexcept paling kuat sekaligus paling mahal untuk dicapai, dan biasanya disisipkan untuk operasi kecil seperti destructor dan move.

Membuktikan Basic vs Strong dengan Program #

Dua class berikut menjalankan operasi yang sama — menambah barang ke tas dengan aturan “barang retak ditolak” — tetapi dengan jaminan berbeda. Yang pertama memodifikasi data langsung; yang kedua memakai idiom copy-and-swap:

cpp
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

// Versi A: hanya jaminan dasar. Setelah kegagalan, objek masih VALID,
// tetapi perubahan sebagian bisa saja sudah terjadi.
class TasDasar {
public:
    void tambah(const std::string& barang) {
        isi_.push_back("kartu " + barang);   // modifikasi dulu...
        if (barang == "retak") {
            throw std::runtime_error("verifikasi gagal untuk barang retak");
        }
    }
    std::size_t jumlah() const { return isi_.size(); }

private:
    std::vector<std::string> isi_;
};

// Versi B: jaminan kuat dengan copy-and-swap. Kerjakan semua di salinan;
// objek asli hanya disentuh SETELAH seluruh operasi pasti berhasil.
class TasKuat {
public:
    void tambah(const std::string& barang) {
        TasKuat salinan(*this);   // salin state saat ini
        salinan.isi_.push_back("kartu " + barang);
        if (barang == "retak") {
            throw std::runtime_error("verifikasi gagal untuk barang retak");
        }
        std::swap(isi_, salinan.isi_);   // tukar hanya saat aman
    }
    std::size_t jumlah() const { return isi_.size(); }

private:
    std::vector<std::string> isi_;
};

int main() {
    TasDasar tasA;
    tasA.tambah("rapat");
    try {
        tasA.tambah("retak");
    } catch (const std::runtime_error& e) {
        std::cout << "TasDasar gagal: " << e.what() << "\n";
    }
    std::cout << "TasDasar berisi " << tasA.jumlah()
              << " item setelah gagal\n";

    TasKuat tasB;
    tasB.tambah("rapat");
    try {
        tasB.tambah("retak");
    } catch (const std::runtime_error& e) {
        std::cout << "TasKuat gagal: " << e.what() << "\n";
    }
    std::cout << "TasKuat berisi " << tasB.jumlah()
              << " item setelah gagal\n";
    return 0;
}

Output yang Dihasilkan:

text
TasDasar gagal: verifikasi gagal untuk barang retak
TasDasar berisi 2 item setelah gagal
TasKuat gagal: verifikasi gagal untuk barang retak
TasKuat berisi 1 item setelah gagal

Bedah kedua versi:

  1. Baris 11-12 (TasDasar::tambah): push_back sudah menambah "kartu retak" SEBELUM verifikasi melempar. Setelah kegagalan, tas berisi 2 item — item yang ditolak tetap tertulis. Objeknya valid (tidak ada kebocoran, vector utuh), tetapi pemanggil tidak bisa mengasumsikan apa-apa soal isi akhirnya. Itulah jaminan basic.
  2. Baris 25-33 (TasKuat::tambah): seluruh kerja dilakukan pada salinan. Saat verifikasi melempar, exception menyapu salinan beserta isinya — objek asli belum tersentuh sedikit pun.
  3. Baris 32 (std::swap(isi_, salinan.isi_)): hanya dijalankan bila semua langkah berhasil; pertukaran isi dua vector adalah operasi murah dan noexcept, sehingga langkah final ini dijamin tidak gagal.
  4. Output TasKuat berisi 1 item: transaksi penuh — tambah("retak") gagal dan tas persis seperti sebelum dipanggil. Itulah jaminan strong.

Idiom di baris 25-32 adalah preview dari copy-and-swap: salin dulu, kerjakan di salinan, tukar di akhir. Idiom ini juga menjadi cara termudah memberi assignment operator yang kuat; pembahasannya lengkap pada Chapter Exception-Safe Resource Management di Section Manajemen Memori. Harganya jelas: satu penyalinan state untuk setiap operasi — itulah sebabnya pustaka standar tidak memberi strong guarantee di semua tempat, dan sebagian operasi (seperti vector::insert) hanya menjanjikan basic.

Peran RAII: Pemberi Jaminan Basic secara Gratis #

Berita pentingnya: bila semua resource yang dipegang sebuah class berada di member RAII — std::string, std::vector, std::unique_ptr, std::ofstream — jaminan basic datang hampir otomatis. Ketika exception menyapu function di tengah operasi, stack unwinding memanggil destructor setiap member yang sudah selesai dibangun, sehingga tidak ada memori, berkas, atau mutex yang tertinggal. Yang tersisa bagi kita hanya menjaga invariant logis: urutan modifikasi yang tidak meninggalkan field berpasangan dalam keadaan tak sinkron. Kombinasi keduanya adalah resep praktis yang dipakai kode C++ modern: member RAII untuk resource, urutan kerja yang hati-hati untuk invariant, dan copy-and-swap pada operasi yang dijanjikan transaksional.

Dengan tiga jaminan ini, kita punya kosakata untuk mendesain class yang aman di tengah exception. Tapi kosakata itu menimbulkan kebutuhan baru: bagaimana menandai — dan memeriksa — bahwa sebuah function memang tidak melempar? Alatnya adalah noexcept, topik utuh chapter berikutnya.

Referensi: cppreference — std::exception , cppreference — std::vector .