Bagian terakhir ini menutup dua celah yang sengaja dibiarkan terbuka: pembagian dengan nol dan hasil perhitungan yang melampaui rentang double. Keduanya menarik karena bukan masalah penguraian — token di dalamnya sah — melainkan masalah aritmetika yang baru tampak setelah perhitungan berjalan. Kita juga menambahkan kontrak exit code agar kalkulator bisa dipakai di dalam script, lalu mengunci seluruh perilaku program, baik yang benar maupun yang ditolak, dengan test harness yang dijalankan sebagai bagian dari proyek. Setelah bagian ini, kalkulator kita bukan sekadar program yang bekerja: ia adalah program yang perilakunya terdokumentasi oleh pengujian.

Lima Jenis Galat dan Kapan Mereka Muncul #

Sampai titik ini JenisGalat sudah mengenal tiga jenis galat dari Bagian 3. Bagian ini menambahkan PembagianNol dan HasilTakBerhingga, sehingga peta lengkapnya menjadi lima:

Jenis galatPemicuDiperiksa di
AngkaTidakSahtoken angka tidak dapat diurai (12abc, +5, nan, 1e400)parseAngka — Bagian 3
OperatorTidakSahtoken operator di luar whitelist (?, ++)evaluasi — Bagian 3
FormatTidakLengkapekspresi kosong atau berakhir setelah operatorevaluasi — Bagian 3
PembagianNolpembagi / atau % bernilai nolterapkan — baru di bagian ini
HasilTakBerhinggahasil akhir inf atau nanevaluasi — baru di bagian ini

Mengapa pemeriksaannya terbagi di dua tempat yang berbeda? Pembagian nol bisa dicegah sebelum operasi dijalankan karena pemicunya terlihat langsung dari operannya. Sebaliknya, 1e308 * 10 memakai dua operan yang sepenuhnya sah — hanya akumulator hasil perhitungan yang meluap — sehingga satu-satunya titik pemeriksaan yang benar adalah setelah seluruh evaluasi selesai.

Kontrak Exit Code #

main kini menghitung setiap galat di dalam jumlahGalat dan mengembalikan 0 bila semua baris berhasil, atau 1 bila minimal satu baris galat. Kontrak ini tampak sepele, tetapi ia yang membuat program bisa diotomasi: script dan pipeline CI menilai keberhasilan lewat exit code, bukan dengan membaca teks keluaran. Perintah seperti ./kalkulator < ekspresi.txt lalu echo $? kini memberi jawaban mesin-baca tentang apakah seluruh berkas ekspresi sah. Pola yang sama kita pertahankan dari Chapter Integration Testing: kontrak exit code adalah bagian dari antarmuka program, bukan efek samping.

Program Lengkap: kalkulator-final.cpp #

Berikut program utuh hasil empat bagian ini — versi final yang juga tersimpan di src/kalkulator.cpp. Blok di bawah sudah dilengkapi skenario masukan, jadi verifier menjalankan program persis seperti pembaca yang menyalurkan deretan ekspresi ke stdin:

cpp
// kalkulator.cpp — kalkulator terminal: REPL baca ekspresi -> parse -> hitung -> cetak.
//
// Kompilasi:
//   g++ -std=c++23 -Wall -Wextra kalkulator.cpp -o kalkulator
// Pakai:
//   ./kalkulator                 (interaktif: ketik ekspresi, akhiri dengan Ctrl+D)
//   ./kalkulator < ekspresi.txt  (batch: satu ekspresi per baris)
//
// Kontrak exit code:
//   0 — semua baris dievaluasi berhasil (atau masukan kosong)
//   1 — ada minimal satu baris yang galat

#include <charconv>      // std::from_chars
#include <cmath>         // std::fmod, std::isfinite
#include <cstddef>       // std::size_t
#include <expected>      // std::expected, std::unexpected
#include <iostream>      // std::getline, std::cin
#include <print>         // std::println
#include <string>
#include <string_view>
#include <vector>

// ---------- jenis galat dan pesannya ----------

enum class JenisGalat {
    AngkaTidakSah,       // token yang seharusnya angka tidak dapat diurai
    OperatorTidakSah,    // token yang seharusnya operator tidak dikenal
    FormatTidakLengkap,  // susunan token tidak membentuk ekspresi utuh
    PembagianNol,        // pembagi bernilai nol
    HasilTakBerhingga,   // hasil meluap menjadi inf atau nan
};

struct Galat {
    JenisGalat jenis;
    std::string rincian;  // token penyebab atau konteks tambahan
};

std::string pesanGalat(const Galat& g) {
    switch (g.jenis) {
        case JenisGalat::AngkaTidakSah:
            return "angka tidak sah: '" + g.rincian + "'";
        case JenisGalat::OperatorTidakSah:
            return "operator tidak sah: '" + g.rincian + "'";
        case JenisGalat::FormatTidakLengkap:
            return "format tidak lengkap: " + g.rincian;
        case JenisGalat::PembagianNol:
            return "pembagian dengan nol tidak terdefinisi";
        case JenisGalat::HasilTakBerhingga:
            return "hasil tidak berhingga (melebihi rentang double)";
    }
    return "galat tidak diketahui";
}

// ---------- tahap 1: tokenisasi ----------

std::vector<std::string_view> tokenisasi(std::string_view baris) {
    std::vector<std::string_view> hasil;
    std::size_t i = 0;
    while (i < baris.size()) {
        while (i < baris.size() && (baris[i] == ' ' || baris[i] == '\t')) {
            ++i;  // lewati spasi di antara token
        }
        std::size_t mulai = i;
        while (i < baris.size() && baris[i] != ' ' && baris[i] != '\t') {
            ++i;
        }
        if (i > mulai) {
            hasil.push_back(baris.substr(mulai, i - mulai));
        }
    }
    return hasil;
}

// ---------- tahap 2: parsing angka ----------

std::expected<double, Galat> parseAngka(std::string_view token) {
    double nilai = 0.0;
    const char* akhir = token.data() + token.size();
    auto [ptr, ec] = std::from_chars(token.data(), akhir, nilai);
    if (ec != std::errc{} || ptr != akhir) {
        // ec != errc{} : token bukan angka sama sekali, atau meluap (mis. 1e400)
        // ptr != akhir : "12abc" — angka terurai tetapi ada sisa karakter
        return std::unexpected(Galat{JenisGalat::AngkaTidakSah, std::string(token)});
    }
    if (!std::isfinite(nilai)) {
        // from_chars menerima "nan" dan "inf" — kalkulator ini menolaknya
        return std::unexpected(Galat{JenisGalat::AngkaTidakSah, std::string(token)});
    }
    return nilai;
}

// ---------- tahap 3: operasi aritmetika ----------

bool adalahOperator(std::string_view token) {
    return token == "+" || token == "-" || token == "*"
        || token == "/" || token == "%";
}

std::expected<double, Galat> terapkan(std::string_view op, double kiri,
                                      double kanan) {
    if (op == "+") return kiri + kanan;
    if (op == "-") return kiri - kanan;
    if (op == "*") return kiri * kanan;
    if (kanan == 0.0) {
        // berlaku untuk / dan %: keduanya tidak terdefinisi saat pembagi nol
        return std::unexpected(Galat{JenisGalat::PembagianNol, ""});
    }
    if (op == "/") return kiri / kanan;
    return std::fmod(kiri, kanan);  // op == "%": sisa pembagian riil
}

// ---------- tahap 4: evaluasi ekspresi ----------

std::expected<double, Galat> evaluasi(std::string_view baris) {
    const auto token = tokenisasi(baris);
    if (token.empty()) {
        return std::unexpected(
            Galat{JenisGalat::FormatTidakLengkap, "ekspresi kosong"});
    }

    auto nilaiPertama = parseAngka(token[0]);
    if (!nilaiPertama) {
        return nilaiPertama;  // meneruskan galat ke pemanggil
    }
    double akumulator = *nilaiPertama;

    // susunan wajib: angka (operator angka)* — evaluasi kiri ke kanan
    std::size_t i = 1;
    while (i < token.size()) {
        if (!adalahOperator(token[i])) {
            return std::unexpected(
                Galat{JenisGalat::OperatorTidakSah, std::string(token[i])});
        }
        if (i + 1 >= token.size()) {
            return std::unexpected(Galat{
                JenisGalat::FormatTidakLengkap,
                "ekspresi berakhir setelah operator '" + std::string(token[i])
                    + "'"});
        }
        auto kanan = parseAngka(token[i + 1]);
        if (!kanan) {
            return kanan;
        }
        auto hasilOperasi = terapkan(token[i], akumulator, *kanan);
        if (!hasilOperasi) {
            return hasilOperasi;
        }
        akumulator = *hasilOperasi;
        i += 2;
    }

    if (!std::isfinite(akumulator)) {
        return std::unexpected(Galat{JenisGalat::HasilTakBerhingga, ""});
    }
    return akumulator;
}

// ---------- antarmuka: loop REPL ----------

int main() {
    int jumlahGalat = 0;
    std::string baris;
    while (std::getline(std::cin, baris)) {
        std::string_view masukan = baris;
        if (masukan.find_first_not_of(" \t") == std::string_view::npos) {
            continue;  // baris kosong atau hanya spasi: diabaikan
        }
        auto hasil = evaluasi(masukan);
        if (hasil) {
            std::println("{}", *hasil);
        } else {
            ++jumlahGalat;
            std::println("galat: {}", pesanGalat(hasil.error()));
        }
    }
    return jumlahGalat == 0 ? 0 : 1;
}

Skenario di atas sengaja mencampur ekspresi sah dan jahil agar setiap jalur keluaran terlihat sekali jalan.

Output yang Dihasilkan:

text
42
galat: pembagian dengan nol tidak terdefinisi
galat: angka tidak sah: '12abc'
20
galat: format tidak lengkap: ekspresi berakhir setelah operator '-'
galat: hasil tidak berhingga (melebihi rentang double)
galat: angka tidak sah: 'nan'
1

Mari kita bedah perubahan yang baru dibandingkan versi Bagian 3:

  1. terapkan mengembalikan std::expected<double, Galat> — tipe kembaliannya berubah dari double karena operasi kini bisa gagal. Sebelum menerapkan / maupun %, function memeriksa kanan == 0.0 dan mengembalikan PembagianNol bila benar. Pemeriksaan dilakukan sebelum pembagian, bukan sesudah: pada double, 5 / 0 tidak melempar apa pun melainkan menghasilkan inf, dan std::fmod(5, 0) menghasilkan nan — keduanya senyap, jadi penangkalnya harus eksplisit.
  2. evaluasi meneruskan kegagalan operasi — setiap hasilOperasi diperiksa; bila berisi galat, evaluasi berhenti dan meneruskan expected itu ke pemanggil tanpa membungkus ulang. Ekspresi panjang berhenti pada kesalahan pertama, bukan melanjutkan perhitungan dengan nilai tak berhingga.
  3. Pemeriksaan akhir std::isfinite(akumulator) — setelah loop selesai, hasil akhir disaring sekali lagi. Inilah yang menangkap 1e308 * 10: kedua operannya sah, tetapi akumulator meluap menjadi inf, dan pengguna menerima HasilTakBerhingga alih-alih huruf inf yang tidak bermakna.
  4. main menutup kontrak exit code — penghitung jumlahGalat bertambah setiap kali sebuah baris menghasilkan galat, baris kosong tetap diabaikan tanpa dihitung, dan return di akhir menerjemahkan penghitung menjadi 0 atau 1.

Mengunci Perilaku dengan Test Harness #

Langkah terakhir proyek: test harness. Harness ini mengikuti pola Chapter Unit Testing — macro MULAI_UJI, CHECK_TRUE, CHECK_EQ, dan CHECK_GALAT, penghitung lulus/gagal, serta gaya Arrange-Act-Assert — tetapi ada satu syarat yang membuatnya mungkin: semua logika kalkulator adalah function murni dari Bagian 1, sehingga harness bisa memanggil evaluasi("2 + 3 * 4") langsung dengan string literal, tanpa menyalurkan stdin dan tanpa menangkap keluaran. Harness menyalin function yang diuji ke dalam satu berkas agar bisa dikompilasi sendiri; cara memecahnya menjadi header dan source dibahas pada Chapter yang membahas separate compilation.

cpp
// uji-kalkulator.cpp — mini test harness untuk fungsi-fungsi kalkulator.
// Pola mengikuti harness-uji.cpp pada Chapter Unit Testing: macro CHECK,
// penghitung lulus/gagal, dan kontrak exit code.
//
// Kompilasi:
//   g++ -std=c++23 -Wall -Wextra uji-kalkulator.cpp -o uji-kalkulator
// Jalankan:
//   ./uji-kalkulator    (exit 0 = semua pengujian lulus, exit 1 = ada yang gagal)

#include <charconv>      // std::from_chars
#include <cmath>         // std::fmod, std::isfinite
#include <cstddef>       // std::size_t
#include <expected>      // std::expected, std::unexpected
#include <iostream>
#include <string>
#include <string_view>
#include <vector>

// ---------- penghitung harness ----------

static int jumlahLulus = 0;
static int jumlahGagal = 0;
static std::string namaUjiAktif;

#define MULAI_UJI(nama) namaUjiAktif = (nama)
#define CHECK_TRUE(kondisi, deskripsi) \
    cekBoolean((kondisi), (deskripsi), #kondisi)
#define CHECK_EQ(aktual, diharapkan) \
    cekSama((aktual), (diharapkan), #aktual)
#define CHECK_GALAT(hasil, jenisDiharapkan, deskripsi) \
    cekGalat((hasil), JenisGalat::jenisDiharapkan, (deskripsi))

void cekBoolean(bool kondisi, const std::string& deskripsi,
                const char* ekspresi) {
    if (kondisi) {
        ++jumlahLulus;
        std::cout << "[ LULUS ] " << namaUjiAktif << " - " << deskripsi << "\n";
    } else {
        ++jumlahGagal;
        std::cout << "[ GAGAL ] " << namaUjiAktif << " - " << deskripsi
                  << " (" << ekspresi << " bernilai false)\n";
    }
}

template <typename T, typename U>
void cekSama(const T& aktual, const U& diharapkan, const char* ekspresi) {
    if (aktual == diharapkan) {
        ++jumlahLulus;
        std::cout << "[ LULUS ] " << namaUjiAktif << " - " << ekspresi << "\n";
    } else {
        ++jumlahGagal;
        std::cout << "[ GAGAL ] " << namaUjiAktif << " - " << ekspresi
                  << " (aktual " << aktual << ", diharapkan " << diharapkan
                  << ")\n";
    }
}

// ---------- kode yang diuji (identik dengan src/kalkulator.cpp) ----------

enum class JenisGalat {
    AngkaTidakSah,
    OperatorTidakSah,
    FormatTidakLengkap,
    PembagianNol,
    HasilTakBerhingga,
};

struct Galat {
    JenisGalat jenis;
    std::string rincian;
};

std::string pesanGalat(const Galat& g) {
    switch (g.jenis) {
        case JenisGalat::AngkaTidakSah:
            return "angka tidak sah: '" + g.rincian + "'";
        case JenisGalat::OperatorTidakSah:
            return "operator tidak sah: '" + g.rincian + "'";
        case JenisGalat::FormatTidakLengkap:
            return "format tidak lengkap: " + g.rincian;
        case JenisGalat::PembagianNol:
            return "pembagian dengan nol tidak terdefinisi";
        case JenisGalat::HasilTakBerhingga:
            return "hasil tidak berhingga (melebihi rentang double)";
    }
    return "galat tidak diketahui";
}

std::vector<std::string_view> tokenisasi(std::string_view baris) {
    std::vector<std::string_view> hasil;
    std::size_t i = 0;
    while (i < baris.size()) {
        while (i < baris.size() && (baris[i] == ' ' || baris[i] == '\t')) {
            ++i;
        }
        std::size_t mulai = i;
        while (i < baris.size() && baris[i] != ' ' && baris[i] != '\t') {
            ++i;
        }
        if (i > mulai) {
            hasil.push_back(baris.substr(mulai, i - mulai));
        }
    }
    return hasil;
}

std::expected<double, Galat> parseAngka(std::string_view token) {
    double nilai = 0.0;
    const char* akhir = token.data() + token.size();
    auto [ptr, ec] = std::from_chars(token.data(), akhir, nilai);
    if (ec != std::errc{} || ptr != akhir) {
        return std::unexpected(Galat{JenisGalat::AngkaTidakSah, std::string(token)});
    }
    if (!std::isfinite(nilai)) {
        return std::unexpected(Galat{JenisGalat::AngkaTidakSah, std::string(token)});
    }
    return nilai;
}

bool adalahOperator(std::string_view token) {
    return token == "+" || token == "-" || token == "*"
        || token == "/" || token == "%";
}

std::expected<double, Galat> terapkan(std::string_view op, double kiri,
                                      double kanan) {
    if (op == "+") return kiri + kanan;
    if (op == "-") return kiri - kanan;
    if (op == "*") return kiri * kanan;
    if (kanan == 0.0) {
        return std::unexpected(Galat{JenisGalat::PembagianNol, ""});
    }
    if (op == "/") return kiri / kanan;
    return std::fmod(kiri, kanan);
}

std::expected<double, Galat> evaluasi(std::string_view baris) {
    const auto token = tokenisasi(baris);
    if (token.empty()) {
        return std::unexpected(
            Galat{JenisGalat::FormatTidakLengkap, "ekspresi kosong"});
    }

    auto nilaiPertama = parseAngka(token[0]);
    if (!nilaiPertama) {
        return nilaiPertama;
    }
    double akumulator = *nilaiPertama;

    std::size_t i = 1;
    while (i < token.size()) {
        if (!adalahOperator(token[i])) {
            return std::unexpected(
                Galat{JenisGalat::OperatorTidakSah, std::string(token[i])});
        }
        if (i + 1 >= token.size()) {
            return std::unexpected(Galat{
                JenisGalat::FormatTidakLengkap,
                "ekspresi berakhir setelah operator '" + std::string(token[i])
                    + "'"});
        }
        auto kanan = parseAngka(token[i + 1]);
        if (!kanan) {
            return kanan;
        }
        auto hasilOperasi = terapkan(token[i], akumulator, *kanan);
        if (!hasilOperasi) {
            return hasilOperasi;
        }
        akumulator = *hasilOperasi;
        i += 2;
    }

    if (!std::isfinite(akumulator)) {
        return std::unexpected(Galat{JenisGalat::HasilTakBerhingga, ""});
    }
    return akumulator;
}

// ---------- helper harness untuk tipe galat kalkulator ----------

void cekGalat(const std::expected<double, Galat>& hasil, JenisGalat jenis,
              const std::string& deskripsi) {
    if (!hasil && hasil.error().jenis == jenis) {
        ++jumlahLulus;
        std::cout << "[ LULUS ] " << namaUjiAktif << " - " << deskripsi << "\n";
    } else {
        ++jumlahGagal;
        std::cout << "[ GAGAL ] " << namaUjiAktif << " - " << deskripsi << "\n";
    }
}

// ---------- test case, gaya Arrange-Act-Assert ----------

void ujiOperasiDasar() {
    MULAI_UJI("operasi dasar");
    CHECK_EQ(evaluasi("12 + 30").value_or(-1.0), 42.0);
    CHECK_EQ(evaluasi("100 / 4").value_or(-1.0), 25.0);
    CHECK_EQ(evaluasi("7 / 2").value_or(-1.0), 3.5);
    CHECK_EQ(evaluasi("12 % 5").value_or(-1.0), 2.0);
}

void ujiUrutanKiriKeKanan() {
    MULAI_UJI("evaluasi kiri ke kanan");
    CHECK_EQ(evaluasi("2 + 3 * 4").value_or(-1.0), 20.0);   // bukan 14
    CHECK_EQ(evaluasi("10 - 2 - 3").value_or(-1.0), 5.0);
    CHECK_EQ(evaluasi("20 / 2 * 2").value_or(-1.0), 20.0);
}

void ujiSpasi() {
    MULAI_UJI("penanganan spasi");
    CHECK_TRUE(tokenisasi("  12   +   30 ").size() == 3,
               "spasi ganda tetap menghasilkan 3 token");
    CHECK_EQ(evaluasi("  12   +   30 ").value_or(-1.0), 42.0);
    CHECK_EQ(evaluasi("12\t+\t30").value_or(-1.0), 42.0);
}

void ujiAngkaTidakSah() {
    MULAI_UJI("angka tidak sah");
    CHECK_GALAT(evaluasi("12abc + 3"), AngkaTidakSah, "sisa karakter setelah angka ditolak");
    CHECK_GALAT(evaluasi("+5 * 2"), AngkaTidakSah, "tanda plus di depan ditolak");
    CHECK_GALAT(evaluasi("nan + 1"), AngkaTidakSah, "token nan ditolak");
    CHECK_GALAT(evaluasi("1e400 * 1"), AngkaTidakSah, "angka meluap ditolak");
}

void ujiOperatorTidakSah() {
    MULAI_UJI("operator tidak sah");
    CHECK_GALAT(evaluasi("12 ? 3"), OperatorTidakSah, "tanda tanya bukan operator");
    CHECK_GALAT(evaluasi("12 ++ 3"), OperatorTidakSah, "operator ganda ditolak");
}

void ujiFormatTidakLengkap() {
    MULAI_UJI("format tidak lengkap");
    CHECK_GALAT(evaluasi("12 +"), FormatTidakLengkap, "ekspresi berakhir setelah operator");
    CHECK_GALAT(evaluasi(""), FormatTidakLengkap, "ekspresi kosong");
}

void ujiPembagianNol() {
    MULAI_UJI("pembagian nol");
    CHECK_GALAT(evaluasi("5 / 0"), PembagianNol, "pembagian nol terdeteksi");
    CHECK_GALAT(evaluasi("12 % 0"), PembagianNol, "modulo nol terdeteksi");
}

void ujiHasilTakBerhingga() {
    MULAI_UJI("hasil tak berhingga");
    CHECK_GALAT(evaluasi("1e308 * 10"), HasilTakBerhingga, "hasil meluap terdeteksi");
}

void ujiPesanGalat() {
    MULAI_UJI("pesan galat");
    CHECK_EQ(pesanGalat({JenisGalat::PembagianNol, ""}),
             std::string("pembagian dengan nol tidak terdefinisi"));
    CHECK_EQ(pesanGalat({JenisGalat::AngkaTidakSah, "12abc"}),
             std::string("angka tidak sah: '12abc'"));
}

int main() {
    ujiOperasiDasar();
    ujiUrutanKiriKeKanan();
    ujiSpasi();
    ujiAngkaTidakSah();
    ujiOperatorTidakSah();
    ujiFormatTidakLengkap();
    ujiPembagianNol();
    ujiHasilTakBerhingga();
    ujiPesanGalat();

    std::cout << "\nringkasan: " << jumlahLulus << " lulus, " << jumlahGagal
              << " gagal\n";
    return jumlahGagal == 0 ? 0 : 1;
}

Output yang Dihasilkan:

text
[ LULUS ] operasi dasar - evaluasi("12 + 30").value_or(-1.0)
[ LULUS ] operasi dasar - evaluasi("100 / 4").value_or(-1.0)
[ LULUS ] operasi dasar - evaluasi("7 / 2").value_or(-1.0)
[ LULUS ] operasi dasar - evaluasi("12 % 5").value_or(-1.0)
[ LULUS ] evaluasi kiri ke kanan - evaluasi("2 + 3 * 4").value_or(-1.0)
[ LULUS ] evaluasi kiri ke kanan - evaluasi("10 - 2 - 3").value_or(-1.0)
[ LULUS ] evaluasi kiri ke kanan - evaluasi("20 / 2 * 2").value_or(-1.0)
[ LULUS ] penanganan spasi - spasi ganda tetap menghasilkan 3 token
[ LULUS ] penanganan spasi - evaluasi("  12   +   30 ").value_or(-1.0)
[ LULUS ] penanganan spasi - evaluasi("12\t+\t30").value_or(-1.0)
[ LULUS ] angka tidak sah - sisa karakter setelah angka ditolak
[ LULUS ] angka tidak sah - tanda plus di depan ditolak
[ LULUS ] angka tidak sah - token nan ditolak
[ LULUS ] angka tidak sah - angka meluap ditolak
[ LULUS ] operator tidak sah - tanda tanya bukan operator
[ LULUS ] operator tidak sah - operator ganda ditolak
[ LULUS ] format tidak lengkap - ekspresi berakhir setelah operator
[ LULUS ] format tidak lengkap - ekspresi kosong
[ LULUS ] pembagian nol - pembagian nol terdeteksi
[ LULUS ] pembagian nol - modulo nol terdeteksi
[ LULUS ] hasil tak berhingga - hasil meluap terdeteksi
[ LULUS ] pesan galat - pesanGalat({JenisGalat::PembagianNol, ""})
[ LULUS ] pesan galat - pesanGalat({JenisGalat::AngkaTidakSah, "12abc"})

ringkasan: 23 lulus, 0 gagal

Sembilan kelompok uji dengan 23 pemeriksaan, semuanya lulus dan program keluar dengan exit code 0. Yang penting dari daftar itu bukan angkanya, melainkan perilaku yang kini terkunci: evaluasi kiri ke kanan (2 + 3 * 4 adalah 20, bukan 14), spasi ganda dan tab tetap sah, seluruh bentuk angka jahil ditolak dengan jenis galat yang tepat, CHECK_GALAT memeriksa jenis galat — bukan sekadar kegagalan — sehingga salah klasifikasi antara PembagianNol dan HasilTakBerhingga akan terdeteksi, dan dua pesan galat diverifikasi kata per kata. Mulai sekarang, perubahan sekecil apa pun pada evaluasi yang mengubah perilaku akan membuat salah satu pemeriksaan merah sebelum sempat merusak pengguna.

Dengan itu proyek kalkulator tuntas: alur REPL dari Bagian 1, aritmetika dari Bagian 2, validasi dari Bagian 3, dan penanganan kesalahan dari bagian ini berkumpul dalam satu program kecil yang rapi. Proyek berikutnya menaikkan kompleksitas satu tingkat — sistem manajemen inventaris dengan struct dan class, operasi CRUD, pencarian dan pengurutan, serta persistensi data ke berkas — dan kita akan memakai kembali hampir semua pola yang baru saja dipakai di sini.

Rujukan resmi: std::expected (cppreference) , std::isfinite (cppreference) .