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:

cpp
#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:

text
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  : -1

Yang bekerja di balik output itu:

  1. Baris 9-13 (enum Galat): setiap penyebab kegagalan diberi nama. Penerima hasil tidak perlu menebak arti angka — bandingkan dengan kode 2 pada Chapter error_code yang baru bermakna setelah diterjemahkan.
  2. 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 pada int biasa maupun std::optional<int>.
  3. Baris 18, 24, 27, 30 (return std::unexpected(...)): cara melaporkan kegagalan. std::unexpected(Galat::Kosong) membungkus nilai galat, dan compiler menyusunnya menjadi expected yang berisi galat. Jalur sukses tinggal return nilai; di baris 32.
  4. Baris 22-28 (std::from_chars): lapisan bawah mendeteksi kegagalan dengan std::errc — keluarga error_code dari chapter sebelumnya. Kita memetakan errc yang generik ke Galat milik domain sendiri, termasuk kasus "25x" yang terurai sebagian (ptr != teks.end()).
  5. Baris 51-55 (has_value(), value(), error()): tiga accessor utama. has_value() bernilai true bila berisi sukses, dan saat itulah value() aman dipanggil; sebaliknya error() memberikan galatnya.
  6. Baris 59-62 (operator bool): konversi ke bool berarti sama dengan has_value() — 1 untuk bernilai, 0 untuk gagal. Bentuk if (hasil) yang ringkas inilah yang paling sering dipakai.
  7. Baris 64-72 (value() yang melempar): memanggil value() pada hasil yang gagal melempar std::bad_expected_access<Galat> — dan objek exception itu membawa galat aslinya, bisa dibaca lewat e.error(). Berbeda dengan std::optional yang operator* pada state kosong berujung undefined behavior, expected justru melempar exception yang tertangkap.
  8. 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:

cpp
#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:

text
umur 25  -> bulan = 300
umur abc -> gagal, value_or = -1
  or_else menampung galat, memakai default
umur dua -> value_or = 0

Bedah alurnya:

  1. Baris 30-39 (keBulan): rantai dua tahap. and_then menjalankan validasi rentang 0-120 — tahap yang bisa gagal lagi, karena itu lambda-nya bertipe kembalian std::expected<int, Galat>. transform kemudian mengalikan 12 — konversi murni yang tidak mungkin gagal, karena itu lambda-nya mengembalikan int biasa.
  2. Baris 42-43 (jalur sukses penuh): bacaUmur("25") menghasilkan 25, validasi lolos, transform menjadi 300. Tidak ada satu if pun di main() untuk mengecek tahapan tengah.
  3. Baris 45-48 (galat menetes): bacaUmur("abc") gagal sejak hulu. and_then dan transform melewati lambda-nya dan hanya meneruskan galat — kode tahap akhir tidak dijalankan. has_value() bernilai false, dan value_or(-1) jatuh ke default.
  4. Baris 51-55 (or_else): lambda ini hanya dijalankan bila gagal, dan boleh mengembalikan expected baru — di sini kita memakai nilai default 0. Jejak cetaknya muncul sebelum baris umur 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:

Aspekstd::optional<T>std::expected<T, E>Exception
Isi saat suksesnilai Tnilai Talur normal, tanpa objek khusus
Isi saat gagalkosong, tanpa alasangalat bertipe Eobjek exception bertipe
Jalur pelaporanreturn valuereturn valuelompatan ke catch terdekat
Biaya saat sukseskecilkecilhampir nol pada model zero-cost
Biaya saat gagalkecilkecilmahal (stack unwinding)
Bila diabaikanalasan kegagalan hilanggalat tetap di dalam box, menunggu dibacatidak 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 .