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 galat | Pemicu | Diperiksa di |
|---|---|---|
AngkaTidakSah | token angka tidak dapat diurai (12abc, +5, nan, 1e400) | parseAngka — Bagian 3 |
OperatorTidakSah | token operator di luar whitelist (?, ++) | evaluasi — Bagian 3 |
FormatTidakLengkap | ekspresi kosong atau berakhir setelah operator | evaluasi — Bagian 3 |
PembagianNol | pembagi / atau % bernilai nol | terapkan — baru di bagian ini |
HasilTakBerhingga | hasil akhir inf atau nan | evaluasi — 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:
// 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:
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'
1Mari kita bedah perubahan yang baru dibandingkan versi Bagian 3:
terapkanmengembalikanstd::expected<double, Galat>— tipe kembaliannya berubah daridoublekarena operasi kini bisa gagal. Sebelum menerapkan/maupun%, function memeriksakanan == 0.0dan mengembalikanPembagianNolbila benar. Pemeriksaan dilakukan sebelum pembagian, bukan sesudah: padadouble,5 / 0tidak melempar apa pun melainkan menghasilkaninf, danstd::fmod(5, 0)menghasilkannan— keduanya senyap, jadi penangkalnya harus eksplisit.evaluasimeneruskan kegagalan operasi — setiaphasilOperasidiperiksa; bila berisi galat, evaluasi berhenti dan meneruskanexpecteditu ke pemanggil tanpa membungkus ulang. Ekspresi panjang berhenti pada kesalahan pertama, bukan melanjutkan perhitungan dengan nilai tak berhingga.- Pemeriksaan akhir
std::isfinite(akumulator)— setelah loop selesai, hasil akhir disaring sekali lagi. Inilah yang menangkap1e308 * 10: kedua operannya sah, tetapi akumulator meluap menjadiinf, dan pengguna menerimaHasilTakBerhinggaalih-alih hurufinfyang tidak bermakna. mainmenutup kontrak exit code — penghitungjumlahGalatbertambah setiap kali sebuah baris menghasilkan galat, baris kosong tetap diabaikan tanpa dihitung, danreturndi akhir menerjemahkan penghitung menjadi0atau1.
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.
// 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:
[ 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 gagalSembilan 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) .