Versi dari Bagian 2 baru terlihat benar selama pengguna berperilaku baik: satu huruf asing di dalam angka sudah cukup untuk melumpuhkan program, dan stod("12abc") diam-diam menerima 12 lalu membuang sisanya. Bagian ini membayar utang tersebut dengan mengganti pengurai angka, menegakkan whitelist operator, dan memeriksa kelengkapan format ekspresi. Prinsip yang kita pegang tetap sama seperti Bagian 1: logika pengolahan berupa function murni yang melaporkan kegagalan lewat tipe kembalian, bukan lewat pencetakan di tempat sembarangan. Setelah bagian ini, setiap masukan jahil menghasilkan satu baris pesan galat yang menyebutkan token penyebabnya — dan dua celah terakhir (pembagian nol serta hasil meluap) kita tuntaskan di Bagian 4.

Dua Utang dari std::stod #

Sebelum mengganti kode, kita rumuskan dulu persisnya apa yang salah dengan std::stod versi Bagian 2. Utang pertama: bila teks bukan angka sama sekali, stod melempar std::invalid_argument; kita tidak menangkapnya, jadi main terhenti dan program keluar tanpa pesan yang bermakna. Utang kedua lebih licik: stod("12abc") mengembalikan 12 dan membuang abc tanpa tanda apa pun — kegagalan yang tidak terlihat jauh lebih sulit dilacak daripada kegagalan yang teriak. Keduanya bermuara pada pilihan alat yang keliru, dan std::from_chars dari header <charconv> memperbaiki keduanya sekaligus:

Aspekstd::stodstd::from_chars
Pelaporan kegagalanexception std::invalid_argumentkode std::errc + pointer posisi akhir
Penerimaan parsial"12abc" menjadi 12 diam-diamterdeteksi lewat pointer akhir
Alokasi memoriperlu salinan std::stringtanpa alokasi, bekerja di atas char*
Terpengaruh localeyatidak

Baris ketiga dan keempat tabel itu adalah bonus yang tidak kita minta tetapi menyenangkan: penguraian bekerja langsung di atas jendela string_view tanpa menyalin apa pun, dan hasilnya bebas dari pengaruh pengaturan locale komputer pengguna. Untuk parser yang dijalankan jutaan kali, perbedaan alokasi bukan sekadar gaya.

Mengurai Angka dengan std::from_chars #

Sekarang kita ganti tahap 2. Dua jenis galat baru juga kita daftarkan di enum JenisGalat — OperatorTidakSah dan FormatTidakLengkap menyusul di section berikutnya — lengkap dengan pesannya:

cpp kalkulator-v2.cpp (potongan: jenis galat dan pengurai angka)
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
};

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;
    }
    return "galat tidak diketahui";
}

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;
}

Mari kita bedah bagian yang berubah dari versi Bagian 2:

  1. const char* akhir = token.data() + token.size(); — from_chars berbicara dalam pasangan pointer awal-akhir, bukan string_view. Pointer ini menunjuk tepat satu posisi setelah karakter terakhir token, menjadi garis finis yang harus dicapai parser.
  2. auto [ptr, ec] = std::from_chars(...) — structured binding membuka dua informasi sekaligus: ptr adalah posisi parser berhenti, dan ec adalah kode kesalahan. Function ini tidak pernah melempar exception; semua kegagalan dilaporkan lewat ec.
  3. if (ec != std::errc{} || ptr != akhir) — dua pemeriksaan dalam satu gerbang. ec berisi result_out_of_range bila angka melebihi rentang double (misalnya 1e400), atau invalid_argument bila tidak ada angka yang terurai. ptr != akhir menangkap utang kedua dari stod: bila parser berhenti sebelum garis finis, ada karakter sisa seperti abc pada 12abc — kita tolak seluruh token, bukan menerima parsialnya.
  4. if (!std::isfinite(nilai)) — celah yang jarang disadari: from_chars untuk double menerima teks nan dan inf. Karena kalkulator ini tidak mengenal keduanya sebagai angka sah, kita saring sekali lagi sebelum mengembalikan nilai.
  5. return std::unexpected(Galat{...}) — kegagalan dikemas sebagai nilai expected, lengkap dengan token aslinya di rincian. Pesan galat yang menyebut token persis seperti ketikan pengguna jauh lebih mudah dijelaskan daripada pesan generik.

Menolak Operator Asing dan Format Tidak Utuh #

Parsing angka yang ketat saja tidak cukup: 12 ? 3 tetap harus ditolak, dan 12 + yang berakhir tanpa operan juga. Semua pemeriksaan itu menumpang pada evaluasi, yang tipe kembaliannya berubah dari double menjadi std::expected<double, Galat>:

cpp kalkulator-v2.cpp (potongan: evaluasi dan main)
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;
        }
        akumulator = terapkan(token[i], akumulator, *kanan);
        i += 2;
    }
    return akumulator;
}

int main() {
    std::string baris;
    while (std::getline(std::cin, baris)) {
        auto hasil = evaluasi(baris);
        if (hasil) {
            std::println("{}", *hasil);
        } else {
            std::println("galat: {}", pesanGalat(hasil.error()));
        }
    }
}

Tiga keputusan di sini patut digarisbawahi. Pertama, kegagalan diteruskan apa adanya: return nilaiPertama; mengembalikan objek expected yang masih berisi galat tanpa membungkusnya ulang — pola yang menjaga jejak kegagalan tetap utuh sampai ke pemanggil. Kedua, whitelist adalahOperator dari Bagian 2 akhirnya bekerja sebagai penjaga: token di posisi operator yang tidak ada dalam daftar lima karakter langsung ditolak dengan jenis OperatorTidakSah. Ketiga, pemeriksaan i + 1 >= token.size() memastikan operator selalu punya operan kanan; tanpa itu, akses token[i + 1] pada ekspresi 12 + akan membaca di luar batas vector. main yang baru membuka hasilnya dengan operator bool milik expected: sukses dicetak, galat dicetak sebagai pesan.

Mencoba Validasi #

Kompilasi versi baru ini lalu serahkan deretan masukan jahil — satu per satu jenis pelanggaran yang sudah kita bahas:

bash
g++ -std=c++23 -Wall -Wextra kalkulator-v2.cpp -o kalkulator-v2
printf '12 + 30\n12abc + 3\n+5 * 2\n12 ? 3\n12 ++ 3\n12 +\n1e400 + 1\nnan + 1\n' | ./kalkulator-v2

Output yang Dihasilkan:

text
42
galat: angka tidak sah: '12abc'
galat: angka tidak sah: '+5'
galat: operator tidak sah: '?'
galat: operator tidak sah: '++'
galat: format tidak lengkap: ekspresi berakhir setelah operator '+'
galat: angka tidak sah: '1e400'
galat: angka tidak sah: 'nan'

Setiap baris galat memetakan ke satu jenis pemeriksaan. 12abc tertangkap ptr != akhir; +5 tertangkap karena format from_chars hanya menerima tanda minus, bukan plus; ? dan ++ tertangkap whitelist; 12 + tertangkap pemeriksaan operan kanan; 1e400 meluap dan masuk lewat ec; nan lolos penguraian lalu tertahan std::isfinite. Perhatikan pula bahwa program tidak berhenti di galat pertama — loop REPL tetap membaca baris berikutnya, persis seperti perilaku shell yang baik.

Masih Ada yang Lolos #

Validasi input sudah rapat, tetapi dua celah tersisa berada di sisi aritmetika, bukan di sisi penguraian:

bash
printf '5 / 0\n1e308 * 10\n' | ./kalkulator-v2

Output yang Dihasilkan:

text
inf
inf

5 / 0 pada double tidak melempar apa pun; standar menentukan hasilnya inf. Begitu pula 1e308 * 10 yang melampaui rentang double di tengah perhitungan. Program tetap “berhasil” mencetak inf — jawaban yang sah secara aritmetika floating-point tetapi tidak bermakna bagi pengguna kalkulator. Bagian 4 menutup kedua celah ini, menambahkan kontrak exit code, lalu mengunci seluruh perilaku dengan test harness.

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