Chapter sebelumnya ditutup dengan janji: ada tipe yang membawa nilai ATAU kegagalan sebagai return value biasa. Janji itu bernama std::expected<T, E> dari header <expected>, distandardkan di C++23 dan sudah berjalan di g++ 14.2 yang kita pakai. Kontraknya lugas: sebuah expected<T, E> selalu berisi salah satu dari dua kemungkinan — nilai sukses bertipe T, atau galat bertipe E yang dibungkus std::unexpected. Untuk kegagalan yang memang diharapkan terjadi, seperti parsing input pengguna, bentuk ini paling pas: alurnya lurus seperti error code, tetapi nilai hasilnya tetap terbawa dan kemungkinan gagalnya tercatat jelas di tipe kembalian function.
Nilai dan Galat dalam Satu Tipe #
Mari kita bangun function bacaUmur() yang menerima teks dan mengembalikan umur. Ada empat hal yang bisa terjadi: teks kosong, teks bukan angka, angkanya di luar jangkauan int, atau sukses. Kita daftarkan penyebab kegagalan sebagai enum class Galat, lalu deklarasikan function dengan tipe kembalian std::expected<int, Galat> — int untuk sukses, Galat untuk gagal:
#include <charconv>
#include <expected>
#include <iostream>
#include <string>
#include <string_view>
#include <system_error>
// Galat yang HARIKAN terjadi saat mem-parsing input pengguna.
enum class Galat {
Kosong,
BukanAngka,
DiLuarJangkauan,
};
// Mengembalikan umur ATAU penyebab kegagalan — dua-duanya dalam satu tipe.
std::expected<int, Galat> bacaUmur(std::string_view teks) {
if (teks.empty()) {
return std::unexpected(Galat::Kosong);
}
int nilai{};
auto [ptr, ec] = std::from_chars(teks.begin(), teks.end(), nilai);
if (ec == std::errc::invalid_argument) {
return std::unexpected(Galat::BukanAngka);
}
if (ec == std::errc::result_out_of_range) {
return std::unexpected(Galat::DiLuarJangkauan);
}
if (ptr != teks.end()) {
return std::unexpected(Galat::BukanAngka); // "25x" dsb.
}
return nilai;
}
const char* namaGalat(Galat g) {
switch (g) {
case Galat::Kosong:
return "Kosong";
case Galat::BukanAngka:
return "BukanAngka";
case Galat::DiLuarJangkauan:
return "DiLuarJangkauan";
}
return "?";
}
int main() {
for (std::string_view teks : {"25", "", "dua", "99999999999999999999"}) {
auto hasil = bacaUmur(teks);
std::cout << "input \"" << teks << "\": ";
if (hasil.has_value()) {
std::cout << "sukses, umur = " << hasil.value() << "\n";
} else {
std::cout << "gagal, galat = " << namaGalat(hasil.error()) << "\n";
}
}
// operator bool: konversi ke bool — true berarti bernilai (sukses).
std::cout << "operator bool(\"25\") = " << (bacaUmur("25") ? 1 : 0)
<< "\n";
std::cout << "operator bool(\"dua\") = " << (bacaUmur("dua") ? 1 : 0)
<< "\n";
// value() pada hasil yang gagal melempar std::bad_expected_access.
auto rusak = bacaUmur("dua");
try {
int umur = rusak.value();
std::cout << "value() = " << umur << "\n";
} catch (const std::bad_expected_access<Galat>& e) {
std::cout << "value() melempar, galat tersimpan = "
<< namaGalat(e.error()) << "\n";
}
std::cout << "value_or saat sukses : " << bacaUmur("25").value_or(-1)
<< "\n";
std::cout << "value_or saat gagal : " << bacaUmur("dua").value_or(-1)
<< "\n";
return 0;
}Output yang Dihasilkan:
input "25": sukses, umur = 25
input "": gagal, galat = Kosong
input "dua": gagal, galat = BukanAngka
input "99999999999999999999": gagal, galat = DiLuarJangkauan
operator bool("25") = 1
operator bool("dua") = 0
value() melempar, galat tersimpan = BukanAngka
value_or saat sukses : 25
value_or saat gagal : -1Yang bekerja di balik output itu:
- Baris 9-13 (enum
Galat): setiap penyebab kegagalan diberi nama. Penerima hasil tidak perlu menebak arti angka — bandingkan dengan kode2pada Chapter error_code yang baru bermakna setelah diterjemahkan. - Baris 16 (signature
std::expected<int, Galat>): tipe kembalian mendaftar kedua kemungkinan secara eksplisit. Pembaca prototype langsung tahu function ini bisa gagal dan alasan gagalnya bertipe apa — informasi yang tidak tersedia padaintbiasa maupunstd::optional<int>. - Baris 18, 24, 27, 30 (
return std::unexpected(...)): cara melaporkan kegagalan.std::unexpected(Galat::Kosong)membungkus nilai galat, dan compiler menyusunnya menjadiexpectedyang berisi galat. Jalur sukses tinggalreturn nilai;di baris 32. - Baris 22-28 (
std::from_chars): lapisan bawah mendeteksi kegagalan denganstd::errc— keluarga error_code dari chapter sebelumnya. Kita memetakanerrcyang generik keGalatmilik domain sendiri, termasuk kasus"25x"yang terurai sebagian (ptr != teks.end()). - Baris 51-55 (
has_value(),value(),error()): tiga accessor utama.has_value()bernilaitruebila berisi sukses, dan saat itulahvalue()aman dipanggil; sebaliknyaerror()memberikan galatnya. - Baris 59-62 (operator
bool): konversi keboolberarti sama denganhas_value()—1untuk bernilai,0untuk gagal. Bentukif (hasil)yang ringkas inilah yang paling sering dipakai. - Baris 64-72 (
value()yang melempar): memanggilvalue()pada hasil yang gagal melemparstd::bad_expected_access<Galat>— dan objek exception itu membawa galat aslinya, bisa dibaca lewate.error(). Berbeda denganstd::optionalyangoperator*pada state kosong berujung undefined behavior,expectedjustru melempar exception yang tertangkap. - Baris 74-77 (
value_or(-1)): membaca nilai dengan jaring pengaman — nilai asli bila sukses, argumen pengganti bila gagal. Pas untuk nilai default yang sah.
Operasi Monadic: and_then, transform, or_else #
Karena kegagalan yang diharapkan sering muncul berantai — parse, lalu validasi, lalu konversi — expected menyediakan operasi monadic: merantai beberapa tahapan atas nilai yang “mungkin gagal” tanpa menulis if di setiap sambungan. Aturannya konsisten: and_then meneruskan nilai ke tahap berikutnya yang juga bisa gagal (lambda-nya mengembalikan expected lagi), transform mengubah nilai tanpa kemungkinan gagal baru, dan or_else memberi kesempatan pada jalur gagal untuk pulih:
#include <charconv>
#include <expected>
#include <iostream>
#include <string>
#include <string_view>
#include <system_error>
enum class Galat {
Kosong,
BukanAngka,
DiLuarJangkauan,
};
std::expected<int, Galat> bacaUmur(std::string_view teks) {
if (teks.empty()) {
return std::unexpected(Galat::Kosong);
}
int nilai{};
auto [ptr, ec] = std::from_chars(teks.begin(), teks.end(), nilai);
if (ec == std::errc::invalid_argument || ptr != teks.end()) {
return std::unexpected(Galat::BukanAngka);
}
if (ec == std::errc::result_out_of_range) {
return std::unexpected(Galat::DiLuarJangkauan);
}
return nilai;
}
// Monadic ops: setiap tahap hanya menulis jalur "sukses"-nya.
std::expected<int, Galat> keBulan(std::expected<int, Galat> umur) {
return umur.and_then([](int u) -> std::expected<int, Galat> {
if (u < 0 || u > 120) {
return std::unexpected(Galat::DiLuarJangkauan);
}
return u;
}).transform([](int u) {
return u * 12; // konversi tahun -> bulan
});
}
int main() {
auto hasilA = keBulan(bacaUmur("25"));
std::cout << "umur 25 -> bulan = " << hasilA.value_or(-1) << "\n";
auto hasilB = keBulan(bacaUmur("abc"));
std::cout << "umur abc -> " << (hasilB.has_value() ? "bernilai"
: "gagal")
<< ", value_or = " << hasilB.value_or(-1) << "\n";
// or_else: jalur "gagal" mendapat kesempatan menyelamatkan hasil.
auto hasilC = bacaUmur("dua").or_else([](Galat)
-> std::expected<int, Galat> {
std::cout << " or_else menampung galat, memakai default\n";
return 0;
});
std::cout << "umur dua -> value_or = " << hasilC.value_or(-1) << "\n";
return 0;
}Output yang Dihasilkan:
umur 25 -> bulan = 300
umur abc -> gagal, value_or = -1
or_else menampung galat, memakai default
umur dua -> value_or = 0Bedah alurnya:
- Baris 30-39 (
keBulan): rantai dua tahap.and_thenmenjalankan validasi rentang 0-120 — tahap yang bisa gagal lagi, karena itu lambda-nya bertipe kembalianstd::expected<int, Galat>.transformkemudian mengalikan 12 — konversi murni yang tidak mungkin gagal, karena itu lambda-nya mengembalikanintbiasa. - Baris 42-43 (jalur sukses penuh):
bacaUmur("25")menghasilkan 25, validasi lolos, transform menjadi 300. Tidak ada satuifpun dimain()untuk mengecek tahapan tengah. - Baris 45-48 (galat menetes):
bacaUmur("abc")gagal sejak hulu.and_thendantransformmelewati lambda-nya dan hanya meneruskan galat — kode tahap akhir tidak dijalankan.has_value()bernilaifalse, danvalue_or(-1)jatuh ke default. - Baris 51-55 (
or_else): lambda ini hanya dijalankan bila gagal, dan boleh mengembalikanexpectedbaru — di sini kita memakai nilai default0. Jejak cetaknya muncul sebelum barisumur dua -> ...karena lambda tereksekusi saat ekspresi di baris 51 dievaluasi, bukan saat dicetak.
Membandingkan optional, expected, dan Exception #
Ketiga alat ini sering dipertaruhkan pada masalah yang sama, dan perbedaannya terletak pada apa yang mereka katakan saat gagal. std::optional menjawab “ada atau tidak ada” — tanpa alasan. std::expected menjawab “bernilai, atau gagal karena ini”. Exception melaporkan lewat alur terpisah yang bisa dilompati berlapis-lapis ke atas. Ringkasannya:
| Aspek | std::optional<T> | std::expected<T, E> | Exception |
|---|---|---|---|
| Isi saat sukses | nilai T | nilai T | alur normal, tanpa objek khusus |
| Isi saat gagal | kosong, tanpa alasan | galat bertipe E | objek exception bertipe |
| Jalur pelaporan | return value | return value | lompatan ke catch terdekat |
| Biaya saat sukses | kecil | kecil | hampir nol pada model zero-cost |
| Biaya saat gagal | kecil | kecil | mahal (stack unwinding) |
| Bila diabaikan | alasan kegagalan hilang | galat tetap di dalam box, menunggu dibaca | tidak bisa — tidak ditangkap berujung terminate |
Baris terakhir tabel itu penting. error_code yang tidak diperiksa menghilang diam-diam, dan optional yang tidak dibuka menyembunyikan alasannya; sedangkan exception yang tidak ditangkap menghentikan program. expected menumpangkan galat pada hasil itu sendiri: untuk memakai nilainya kita mau tidak mau membuka box — dan pada saat yang sama galatnya ikut tersedia. Karena itu aturan praktisnya: optional untuk “mungkin tidak ada” yang bukan kegagalan, expected untuk kegagalan harian yang penanganannya dekat dengan titik kejadian, dan exception untuk kegagalan luar biasa yang layak dilompati berlapis.
Kini kita memegang tiga alat pelaporan kegagalan sekaligus: exception, error_code, dan expected. Masing-masing punya biaya dan karakter yang berbeda, dan memilih di antara mereka adalah keputusan desain yang berulang di setiap codebase. Chapter terakhir section ini, Memilih Exception atau Error Code, menyusun semuanya menjadi matriks keputusan — lengkap dengan program yang menjalankan dua gaya pada masalah yang sama.
Referensi: cppreference — std::expected , cppreference — std::unexpected , cppreference — bad_expected_access .