C++ menyediakan beberapa mekanisme penanganan error: exception (throw/try/catch), error code (return-based), std::optional (C++17), std::variant (C++17), dan std::expected (C++23). Pilihan tidak universal — exception baik untuk kasus langka dan fatal, error code cocok untuk failure yang rutin dan expected, optional/variant menangani “tidak ada nilai” tanpa overhead. Pada Chapter ini kita akan membandingkan kelima pendekatan, menjelaskan kapan memilih masing-masing, dan menutup dengan RAII yang menjamin pembersihan resource bahkan saat exception terlempar.
Lima Pendekatan #
| Pendekatan | Sintaks | Cocok untuk |
|---|---|---|
| Exception | throw, try/catch | Error langka/fatal; gagal total |
| Error code | return std::error_code | Failure rutin; tidak fatal |
std::optional<T> | optional<T>, nullopt | Mungkin tidak ada nilai |
std::variant<T, E> | tagged union | Sukses ATAU error dengan info |
std::expected<T, E> (C++23) | expected<T, E> | Modern result type |
Mekanisme: Exception dan Stack Unwinding #
Saat throw e dieksekusi, runtime mulai stack unwinding — keluar dari fungsi saat ini, kemudian caller, kemudian caller-of-caller, dst., sampai menemukan catch yang cocok. Setiap scope yang ditinggalkan memanggil destructor objek lokal (RAII). Bila tidak ada catch yang cocok, std::terminate dipanggil → program abort.
Exception mahal saat dilempar (beberapa mikrodetik hingga milidetik) tetapi gratis saat tidak dilempar (zero-cost exception model di mayoritas compiler modern). Karena itu, exception baik untuk failure yang jarang terjadi — happy path tetap cepat.
Error code mahal saat harus diperiksa (branch di setiap call site) tetapi eksplisit. Untuk failure yang rutin (mis. parse gagal, file tidak ditemukan), error code sering lebih jernih.
Kode: Lima Pendekatan Berdampingan #
#include <iostream>
#include <string>
#include <vector>
#include <stdexcept>
#include <system_error>
#include <variant>
#include <optional>
// 1) Exception-based error handling
double divide_exc(double a, double b) {
if (b == 0) throw std::runtime_error("pembagi nol");
return a / b;
}
// 2) Error code (return-based)
std::error_code divide_ec(double a, double b, double& out) {
if (b == 0) return std::make_error_code(std::errc::invalid_argument);
out = a / b;
return {};
}
// 3) std::optional (C++17) — ada/tidak ada nilai
std::optional<double> divide_opt(double a, double b) {
if (b == 0) return std::nullopt;
return a / b;
}
// 4) std::variant (C++17) — result type (sukses | error)
struct DivResult {
std::variant<double, std::string> v;
bool ok() const { return std::holds_alternative<double>(v); }
double value() const { return std::get<double>(v); }
std::string err() const { return std::get<std::string>(v); }
};
DivResult divide_var(double a, double b) {
if (b == 0) return {std::variant<double, std::string>{std::string{"pembagi nol"}}};
return {std::variant<double, std::string>{a / b}};
}
// 5) Exception hierarchy
class MathError : public std::runtime_error {
public:
MathError(const std::string& msg) : std::runtime_error(msg) {}
};
class DivByZero : public MathError {
public:
DivByZero() : MathError("division by zero") {}
};
double safe_div(double a, double b) {
if (b == 0) throw DivByZero{};
return a / b;
}
int main() {
// 1) Exception
try {
double r = divide_exc(10, 0);
std::cout << "hasil: " << r << '\n';
} catch (const std::runtime_error& e) {
std::cout << "exception: " << e.what() << '\n';
}
// 2) Error code
double out;
auto ec = divide_ec(10, 0, out);
if (ec) {
std::cout << "error_code: " << ec.message() << '\n';
} else {
std::cout << "hasil: " << out << '\n';
}
// 3) std::optional
auto r3 = divide_opt(10, 0);
if (r3) {
std::cout << "hasil: " << *r3 << '\n';
} else {
std::cout << "optional: nullopt\n";
}
// value_or — default bila kosong
double dv = divide_opt(10, 0).value_or(-1);
std::cout << "value_or(-1): " << dv << '\n';
// 4) std::variant
auto r4 = divide_var(10, 0);
if (r4.ok()) std::cout << "variant: " << r4.value() << '\n';
else std::cout << "variant err: " << r4.err() << '\n';
// 5) Exception hierarchy
try {
safe_div(5, 0);
} catch (const DivByZero& e) {
std::cout << "caught DivByZero: " << e.what() << '\n';
} catch (const MathError& e) {
std::cout << "caught MathError: " << e.what() << '\n';
} catch (const std::exception& e) {
std::cout << "caught exception: " << e.what() << '\n';
}
// 6) RAII dengan exception safety
std::vector<int> v = {1, 2, 3};
try {
v.push_back(4);
throw std::runtime_error("test");
} catch (...) {
std::cout << "vector masih utuh: " << v.size() << " elemen\n";
// vector tidak bocor — RAII membersihkan otomatis
}
return 0;
}Output yang Dihasilkan:
exception: pembagi nol
error_code: Invalid argument
optional: nullopt
value_or(-1): -1
variant err: pembagi nol
caught DivByZero: division by zero
vector masih utuh: 4 elemenBedah Kode Baris-per-Baris #
throw std::runtime_error("pembagi nol"): melempar exception. Runtime mulai stack unwinding — keluar daridivide_exc, mencaricatchdi caller.catch (const std::runtime_error& e)menangkap, mencetake.what().std::error_code divide_ec(...): kembalikan error code sebagai return value. Caller harus memeriksaif (ec)—ectruthy bila berisi error. Idiomatik untuk error yang rutin diharapkan.std::optional<double> divide_opt(...): kembalikannulloptbila gagal, nilai bila sukses. Caller memeriksaif (r3)lalu dereference*r3. Bersih dan ekspresif.r3.value_or(-1): idiomatik untuk “nilai default bila kosong”. Hindari pengecekan eksplisit — kode lebih ringkas.std::variant<double, std::string>: tagged union — menyimpan salah satu daridoubleataustd::string.std::holds_alternative<double>(v)memeriksa tipe aktif;std::get<double>(v)mengakses nilai.class MathError : public std::runtime_error: exception hierarchy. SubclassDivByZeromemungkinkancatch (const DivByZero&)menangkap specifically, sementaracatch (const MathError&)menangkap semua error matematika. Urutancatchdari specific ke general.catch (...): catch-all — menangkap exception apa pun. Berguna sebagai last-resort cleanup, tetapi tidak memberikan info error.
Exception Safety Guarantees #
Empat level jaminan safety:
- No-throw: fungsi tidak melempar (
noexcept). Destructor, swap, move — wajib no-throw. - Strong: bila gagal, state program kembali seperti sebelum call. Mis.
vector::push_back— bila gagal alokasi, vector tetap utuh. - Basic: bila gagal, program masih valid (tidak corrupt) tetapi state mungkin berubah. Mayoritas STL.
- No guarantee: bila gagal, program mungkin corrupt. Hindari di kode modern.
RAII adalah kunci strong guarantee: bila operasi gagal setelah sebagian resource diakuisisi, destructor objek lokal membersihkan secara otomatis. Contoh di kode: walaupun exception dilempar setelah v.push_back(4), vector tetap valid dan utuh.
Kapan Memilih Apa #
| Skenario | Pilihan |
|---|---|
| Error langka dan fatal (out of memory, corrupted state) | Exception |
| Failure rutin dan diharapkan (parse error, file not found) | Error code atau optional |
| Mungkin tidak ada nilai (lookup miss) | std::optional |
| Sukses atau error dengan info detail | std::variant atau std::expected (C++23) |
| API yang harus noexcept (destructor, move, swap) | noexcept + return code |
| Boundary ke code C | Error code (C tidak paham exception) |
std::expected<T, E> (C++23) #
C++23 menambahkan std::expected<T, E> — tipe result yang berisi nilai T atau error E. Lebih ringkas dari variant:
std::expected<double, std::string> divide(double a, double b) {
if (b == 0) return std::unexpected("pembagi nol");
return a / b;
}
auto r = divide(10, 0);
if (r.has_value()) std::cout << *r << '\n';
else std::cout << r.error() << '\n';Untuk compiler pra-C++23, pola std::variant<T, E> meniru expected secara manual.
Ringkasan #
- Lima mekanisme: exception, error code,
optional,variant,expected(C++23). - Exception gratis saat tidak lempar, mahal saat lempar; cocok untuk failure langka.
- Error code eksplisit dan cepat, cocok untuk failure rutin.
optional<T>bersih untuk “mungkin tidak ada nilai”;variant<T, E>menambah info error.- Tangkap exception by const reference (
catch (const E& e)), bukan by-value (slicing). - Destructor wajib
noexcept— jangan lempar dari destructor. - RAII menjamin pembersihan resource bahkan saat exception — tulis kode dengan strong/basic guarantee.
- Untuk DSA kompetitif,
optionalpaling sering cukup. Untuk pustaka produksi,expected(C++23) adalah pilihan modern.