Setelah membaca sisi penerima (try-catch) pada chapter sebelumnya, kini kita menengok sisi pengirim. Pernyataan throw terlihat sederhana — satu kata plus satu ekspresi — tetapi di belakangnya ada aturan penting tentang bagaimana objek exception diciptakan, disalin, dan dihentikan perjalanannya. Chapter ini menuntaskan tiga hal yang paling sering menjadi sumber kebingungan: mengapa kita melempar by value tetapi menangkap by reference, cara melempar ulang exception dengan throw; tanpa merusak isinya, dan apa yang terjadi ketika constructor ikut melempar — kasus khusus yang perilakunya berbeda dari semua function lain.
Sintaks throw: Melempar By Value, Menangkap By Reference #
Ekspresi di belakang kata throw menentukan tipe dan isi objek exception. Pernyataan throw std::runtime_error("pesan"); membangun objek std::runtime_error — operandnya dievaluasi lalu dipakai untuk menginisialisasi objek exception yang disimpan compiler di wilayah yang aman dari unwinding. Karena objek itu milik compiler dan hidup sampai handler selesai, kita tidak perlu mengalokasikannya dengan new; cukup lempar objek atau temporary. Di sisi penerima, handler menangkapnya lewat reference — catch (const std::runtime_error& e) — sehingga objek yang ditangani adalah objek exception itu sendiri, bukan salinannya, dan tipe turunannya tidak terpotong.
Kebalikannya adalah kombinasi yang salah: melempar pointer. Kode berikut adalah pola yang harus kita hindari:
void proses(int nilai) {
std::runtime_error lokal("nilai tidak valid"); // objek lokal
if (nilai < 0) {
throw &lokal; // BAHAYA: pointer ke objek yang segera berakhir
}
}Ketika propagasi melakukan unwinding, objek lokal ikut dihancurkan bersama stack frame proses() — pointer yang ditangkap handler kemudian menunjuk ke objek yang sudah tidak sah, dan menggunakannya adalah undefined behavior. Aturannya ringkas: lempar by value, tangkap by reference. Dengan dua sisi itu, kepemilikan objek exception jelas ada di tangan compiler, dan handler bekerja pada objek yang pasti hidup.
Rethrow: Meneruskan Exception yang Sama #
Ada situasi di mana satu level ingin ikut campur — mencatat log, merapikan resource — tanpa bertanggung jawab menangani. Setelah mencatat, level itu melempar ulang exception yang sama dengan pernyataan throw; tanpa operand. Program berikut mencontohkan pola log-then-rethrow:
#include <iostream>
#include <stdexcept>
#include <string>
void simpanKeArsip(const std::string& nama) {
if (nama.empty()) {
throw std::invalid_argument("nama berkas kosong");
}
std::cout << " tersimpan: " << nama << "\n";
}
void prosesUnggahan(const std::string& nama) {
try {
simpanKeArsip(nama);
} catch (const std::exception& e) {
// Level ini hanya MENCATAT, bukan menangani — lalu meneruskan
// exception yang sama tanpa mengubah apa pun.
std::cout << " [log] unggahan gagal: " << e.what() << "\n";
throw; // rethrow: objek exception asli dilanjutkan perjalanannya
}
}
int main() {
try {
prosesUnggahan("laporan.pdf");
prosesUnggahan(""); // memicu kegagalan di level terdalam
} catch (const std::exception& e) {
std::cout << "main() menerima exception asli: " << e.what() << "\n";
}
return 0;
}Output yang Dihasilkan:
tersimpan: laporan.pdf
[log] unggahan gagal: nama berkas kosong
main() menerima exception asli: nama berkas kosongYang layak dicermati:
- Baris 5-10 (
simpanKeArsip): level terdalam yang mendeteksi kontrak dilanggar dan melemparstd::invalid_argument. - Baris 17 (
throw;): bentuk tanpa operand hanya sah di dalam blok catch; ia meneruskan objek exception yang sedang berjalan apa adanya — tipe aslinya, pesannya, semuanya utuh. - Baris 26-27 (
catchdimain()): handler di level terluar tetap bisa membaca pesan aslinya, membuktikan rethrow tidak mengubah apa pun.
Bedakan throw; dengan throw e; di dalam blok catch. Yang kedua membangun objek exception BARU dari ekspresi e — dan karena e bertipe const std::exception&, objek baru itu bertipe std::exception, bukan tipe aslinya: informasi turunannya terpotong (object slicing) dan catch yang lebih spesifik di level atas tidak akan cocok lagi. Maka untuk melempar ulang, selalu gunakan throw; polos.
Ketika Constructor Melempar #
Constructor tidak bisa mengembalikan nilai, jadi throw adalah satu-satunya cara constructor menyatakan bahwa objeknya gagal dibangun. Perilakunya punya aturan khusus yang layak dibuktikan langsung. Class Koneksi di bawah ini punya satu member yang sudah selesai dibangun, lalu badan constructor melempar:
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
// Anggota pencatat: mencetak saat constructor dan destructor berjalan.
struct Jejak {
std::string nama;
explicit Jejak(std::string n) : nama(std::move(n)) {
std::cout << " constructor " << nama << "\n";
}
~Jejak() {
std::cout << " destructor " << nama << "\n";
}
};
class Koneksi {
public:
Koneksi() : kunci_("kunci") {
std::cout << " badan Koneksi(): mencoba membuka jalur\n";
throw std::runtime_error("jalur tidak bisa dibuka");
}
~Koneksi() {
std::cout << " destructor Koneksi() berjalan\n";
}
private:
Jejak kunci_; // member yang sudah selesai dibangun sebelum badan ctor
};
int main() {
try {
std::cout << "membuat Koneksi...\n";
Koneksi k;
std::cout << "baris ini tidak pernah tercetak\n";
} catch (const std::runtime_error& e) {
std::cout << "tertangkap: " << e.what() << "\n";
}
std::cout << "program tetap sehat\n";
return 0;
}Output yang Dihasilkan:
membuat Koneksi...
constructor kunci
badan Koneksi(): mencoba membuka jalur
destructor kunci
tertangkap: jalur tidak bisa dibuka
program tetap sehatCermati dua fakta pada output:
- Baris
constructor kuncilaludestructor kunci: memberkunci_selesai dibangun sebelum badan constructor berjalan, sehingga saatthrowterjadi, compiler bertanggung jawab membersihkannya — destructorJejakdipanggil sebagai bagian dari unwinding. Anggota yang sudah jadi selalu ikut dibersihkan. - Baris
destructor Koneksi() berjalanTIDAK muncul: karena objekKoneksitidak pernah selesai dibangun, destructor kelasnya tidak dipanggil — dan itu benar, sebab destructor hanya boleh berjalan untuk objek yang pernah ada. Variabelkdimain()dianggap tidak pernah dibuat; baris setelahnya tidak pernah dieksekusi.
Aturan ini justru kabar baik untuk desain kita: constructor boleh dan sering kali layak melempar, karena tidak akan ada objek setengah jadi yang berterbangan. Bila semua resource disimpan sebagai member RAII — std::string, std::vector, std::unique_ptr — compiler membersihkan semuanya otomatis saat konstruksi gagal, tanpa satu baris kode pembersihan tambahan. Pola ini yang membuat kode inisialisasi C++ modern bisa dibaca sepanjang kelopaknya.
Sekarang kita menguasai dua sisi mekanisme exception: melempar dan menangkap. Tinggal satu pertanyaan mendasar — objek apa yang layak dilempar? C++ sudah menyediakan hierarki exception standar yang kaya, dan chapter berikutnya memperkenalkannya satu per satu lengkap dengan kapan masing-masing tepat dipakai.
Referensi: cppreference — throw expression , cppreference — constructors .