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:
| Aspek | std::stod | std::from_chars |
|---|---|---|
| Pelaporan kegagalan | exception std::invalid_argument | kode std::errc + pointer posisi akhir |
| Penerimaan parsial | "12abc" menjadi 12 diam-diam | terdeteksi lewat pointer akhir |
| Alokasi memori | perlu salinan std::string | tanpa alokasi, bekerja di atas char* |
| Terpengaruh locale | ya | tidak |
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:
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:
const char* akhir = token.data() + token.size();—from_charsberbicara dalam pasangan pointer awal-akhir, bukanstring_view. Pointer ini menunjuk tepat satu posisi setelah karakter terakhir token, menjadi garis finis yang harus dicapai parser.auto [ptr, ec] = std::from_chars(...)— structured binding membuka dua informasi sekaligus:ptradalah posisi parser berhenti, danecadalah kode kesalahan. Function ini tidak pernah melempar exception; semua kegagalan dilaporkan lewatec.if (ec != std::errc{} || ptr != akhir)— dua pemeriksaan dalam satu gerbang.ecberisiresult_out_of_rangebila angka melebihi rentangdouble(misalnya1e400), atauinvalid_argumentbila tidak ada angka yang terurai.ptr != akhirmenangkap utang kedua daristod: bila parser berhenti sebelum garis finis, ada karakter sisa sepertiabcpada12abc— kita tolak seluruh token, bukan menerima parsialnya.if (!std::isfinite(nilai))— celah yang jarang disadari:from_charsuntukdoublemenerima teksnandaninf. Karena kalkulator ini tidak mengenal keduanya sebagai angka sah, kita saring sekali lagi sebelum mengembalikan nilai.return std::unexpected(Galat{...})— kegagalan dikemas sebagai nilaiexpected, lengkap dengan token aslinya dirincian. 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>:
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:
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-v2Output yang Dihasilkan:
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:
printf '5 / 0\n1e308 * 10\n' | ./kalkulator-v2Output yang Dihasilkan:
inf
inf5 / 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) .