Exception

throw, try, catch, stack unwinding, dan hierarki exception.

Daftar isi

Exception adalah mekanisme C++ untuk melaporkan error yang muncul di tengah eksekusi program, yaitu dengan melempar objek dari titik kejadian menuju titik yang siap menanganannya. Topik ini membahas pernyataan throw, blok try dan catch, proses stack unwinding yang melepaskan bingkai fungsi saat exception menyeberang, serta hierarki std::exception beserta kelas turunannya. Contoh yang digunakan berupa pembagian aman dan konversi teks ke angka, dua situasi yang kegagalannya mudah diduga.

Mekanisme throw, try, dan catch#

Pernyataan throw menghentikan alur normal fungsi yang sedang berjalan dan memulai pencarian penangan (handler) yang cocok. Objek yang dilempar dapat bertipe apa pun, tetapi praktik yang disarankan adalah melempar objek kelas turunan std::exception, misalnya std::invalid_argument atau std::runtime_error. Pencarian penangan dimulai dari fungsi pelempar menuju pemanggilnya, dan penangan pertama yang cocok digunakan.

Blok try membungkus pernyataan yang mungkin melempar exception, sedangkan setiap klausa catch menyatakan satu tipe yang siap ditangkap. Exception dicocokkan dari klausa pertama secara berurutan, sehingga klausa untuk tipe paling spesifik ditulis lebih dahulu. Menangkap dengan const reference merupakan kebiasaan yang aman karena objek aslinya tidak tersalin ulang dan tidak mengalami object slicing. Object slicing sendiri berupa terpotongnya bagian kelas turunan ketika objek disimpan sebagai kelas dasar.

Program berikut menghitung rata-rata melalui pembagian yang dapat gagal. Setiap fungsi menciptakan objek Penanda yang mencetak jejak saat dibuat dan dihancurkan, sehingga alur exception dapat diamati dari keluarannya.

cpp unwinding-rata-rata.cpp
#include <iostream>
#include <stdexcept>
#include <string>

// objek kecil yang mencetak jejak saat dibuat dan dihancurkan
class Penanda {
public:
    explicit Penanda(std::string nama) : nama(nama) {
        std::cout << "masuk " << nama << "\n";
    }
    ~Penanda() {
        std::cout << "keluar " << nama << "\n";
    }

private:
    std::string nama;
};

double bagi(int pembilang, int pembagi) {
    Penanda jejak("bagi");
    if (pembagi == 0) {
        throw std::invalid_argument("pembagi bernilai nol");
    }
    return static_cast<double>(pembilang) / pembagi;
}

double hitungRataRata(int total, int jumlah) {
    Penanda jejak("hitungRataRata");
    return bagi(total, jumlah);
}

int main() {
    try {
        double rataRata = hitungRataRata(90, 0);
        std::cout << "rata-rata = " << rataRata << "\n";
    } catch (const std::invalid_argument& e) {
        std::cout << "tertangkap: " << e.what() << "\n";
    }
    std::cout << "program berlanjut setelah penanganan\n";
    return 0;
}
text Keluaran
masuk hitungRataRata
masuk bagi
keluar bagi
keluar hitungRataRata
tertangkap: pembagi bernilai nol
program berlanjut setelah penanganan

Pada keluaran tersebut, throw dijalankan di dalam fungsi bagi ketika pembaginya bernilai nol. Pesan yang dicetak berasal dari fungsi anggota what(), yaitu pesan yang disertakan saat objek exception dibuat. Baris program berlanjut setelah penanganan menunjukkan bahwa program tidak berhenti: setelah blok catch selesai, eksekusi berlanjut ke pernyataan sesudahnya.

Stack unwinding#

Stack unwinding adalah proses pelepasan bingkai fungsi satu per satu dari fungsi pelempar menuju fungsi yang memuat blok catch yang cocok. Proses ini bukan lompatan biasa karena sebelum sebuah bingkai dilepaskan, destructor seluruh objek lokal di bingkai tersebut dipanggil secara otomatis dan teratur. Perilaku inilah yang membuat RAII efektif bersama exception, karena sumber daya yang dikelola objek lokal pasti dibebaskan meskipun alurnya terputus oleh exception.

Urutan pada program sebelumnya dapat dibaca ulang dari keluarannya. Fungsi bagi melempar exception sehingga objek Penanda milik bagi dihancurkan lebih dahulu, terlihat dari baris keluar bagi. Eksekusi kemudian kembali ke hitungRataRata, yang tidak memiliki blok catch, sehingga objek Penanda milik fungsi tersebut juga dihancurkan. Pemanggilan destructor berhenti ketika penangan yang cocok ditemukan di main, tepat sebelum blok catch dijalankan.

Hierarki std::exception#

Kelas std::exception adalah akar hierarki exception pustaka standar, dengan fungsi anggota virtual what() sebagai kontrak pesannya. Dua cabang yang paling sering digunakan sehari-hari adalah std::logic_error untuk error yang seharusnya dapat dideteksi sebelum program berjalan, seperti std::invalid_argument dan std::out_of_range, serta std::runtime_error untuk error yang baru terlihat saat program berjalan. Selain kedua cabang tersebut, terdapat kelas untuk kondisi khusus seperti std::bad_alloc yang dilempar ketika alokasi memori gagal.

Exception buatan sendiri sebaiknya diturunkan dari salah satu kelas tersebut, bukan dari std::exception secara langsung. Penurunan dari std::runtime_error membuat kelas baru dapat ditangkap oleh klausa catch yang menerima std::runtime_error, sehingga penangan yang sudah ada tetap bekerja untuk exception baru. Program berikut memperlihatkan hal tersebut dengan kelas KesalahanKonversi untuk konversi teks ke angka.

cpp konversi-exception.cpp
#include <iostream>
#include <stdexcept>
#include <string>

// exception buatan sendiri yang ikut hierarki std::exception
class KesalahanKonversi : public std::runtime_error {
public:
    explicit KesalahanKonversi(const std::string& pesan)
        : std::runtime_error(pesan) {}
};

int keInt(const std::string& teks) {
    if (teks.empty()) {
        throw KesalahanKonversi("teks kosong");
    }
    int hasil = 0;
    for (char c : teks) {
        if (c < '0' || c > '9') {
            throw KesalahanKonversi(std::string("karakter non-digit: ") + c);
        }
        hasil = hasil * 10 + (c - '0');
    }
    return hasil;
}

int main() {
    const std::string daftar[] = {"24", "10x", ""};
    for (const std::string& teks : daftar) {
        try {
            int hasil = keInt(teks);
            std::cout << '"' << teks << "\" = " << hasil << "\n";
        } catch (const std::runtime_error& e) {
            std::cout << '"' << teks << "\" ditolak: " << e.what() << "\n";
        }
    }
    return 0;
}
text Keluaran
"24" = 24
"10x" ditolak: karakter non-digit: x
"" ditolak: teks kosong

Klausa catch pada program di atas menerima const std::runtime_error&, sedangkan objek yang dilempar bertipe KesalahanKonversi. Penanganan tetap berhasil karena penangkapan pada kelas dasar mencakup seluruh kelas turunannya, dan pesan dari what() tetap spesifik karena teks yang disertakan saat pembuatan objek disimpan utuh. Apabila kelak dibutuhkan penangan khusus untuk KesalahanKonversi, klausa catch (const KesalahanKonversi&) cukup diletakkan sebelum klausa yang lebih umum.

Kesalahan umum#

  • Menangkap exception dengan nilai, misalnya catch (std::runtime_error e). Objeknya tersalin dan bagian kelas turunannya terpotong, sehingga what() tidak lagi mewakili tipe aslinya. Perbaikannya dengan menangkap lewat const reference.
  • Menuliskan klausa kelas dasar sebelum klausa kelas turunannya. Klausa catch (const std::exception&) yang mendahului catch (const KesalahanKonversi&) membuat klausa kedua tidak pernah tercapai. Perbaikannya berupa penataan ulang urutan klausa, dan compiler biasanya memberi peringatan atas pola ini.
  • Melempar exception dari dalam destructor. Apabila pelemparan terjadi saat stack unwinding sedang berjalan, std::terminate dipanggil dan programnya berhenti. Perbaikannya dengan menjamin destructor tidak melempar, misalnya menangkap error di dalamnya tanpa membiarkan keluar.
  • Membiarkan blok catch kosong tanpa alasan yang jelas. Error hilang tanpa jejak sehingga pencarian penyebabnya sulit dilakukan. Paling tidak, catat atau cetak pesan exceptionnya sebelum penangan ditutup.

Ringkasan#

  • Exception dilaporkan lewat throw, pencariannya dibungkus dengan try, dan penangkapannya dilakukan klausa catch yang dicocokkan berurutan dari atas ke bawah.
  • Stack unwinding memanggil destructor seluruh objek lokal di setiap bingkai yang dilepaskan, sehingga RAII tetap bekerja meskipun alur eksekusi terputus.
  • Hierarki std::exception menyediakan what() dan kelas siap pakai, sedangkan exception buatan sendiri sebaiknya diturunkan dari std::logic_error atau std::runtime_error agar tetap dapat ditangkap oleh penangan yang sudah ada.

Referensi#