Semua data yang selama ini kita kelola hidup di dalam satu std::vector — dan std::vector tidak tahu apa-apa tentang hari esok. Begitu program berakhir, isi inventaris ikut hilang, dan aplikasi manajemen data yang lupa inputnya setiap kali dijalankan tidak berguna. Bagian ini menambahkan dua perintah, simpan dan muat, yang memindahkan seluruh isi inventaris ke berkas teks dan mengambilnya kembali. Persoalan persistensi ternyata kaya keputusan desain: format barisnya apa, siapa yang menolak baris yang rusak, dan apa yang terjadi bila berkas setengah sah — semua dijawab dengan pola galat bertipe yang sudah kita pegang sejak Bagian 1.

Format Berkas: Satu Baris per Produk #

Kita memilih format teks paling sederhana yang masih bisa diperiksa: satu produk satu baris, field dipisah karakter pipa.

text
P001|Kopi|15000|20
P002|Gula|12000|5

Format teks punya dua keunggulan untuk proyek seukuran ini: bisa diperiksa dengan cat tanpa alat tambahan, dan bisa dibaca ulang program apa pun di masa depan. Tugas mengurai barisnya kita letakkan di function murni tersendiri:

cpp src/inventaris.cpp (potongan: uraiBarisProduk)
// format baris: kode|nama|harga|stok
std::optional<Produk> uraiBarisProduk(std::string_view baris) {
    std::vector<std::string_view> bagian;
    std::size_t mulai = 0;
    while (true) {
        std::size_t pipa = baris.find('|', mulai);
        if (pipa == std::string_view::npos) {
            bagian.push_back(baris.substr(mulai));
            break;
        }
        bagian.push_back(baris.substr(mulai, pipa - mulai));
        mulai = pipa + 1;
    }
    if (bagian.size() != 4) {
        return std::nullopt;
    }

    auto harga = parseAngka<long long>(bagian[2]);
    auto stok = parseAngka<int>(bagian[3]);
    if (!harga || !stok) {
        return std::nullopt;
    }

    Produk p{std::string(bagian[0]), std::string(bagian[1]), *harga, *stok};
    if (!p.sah()) {
        return std::nullopt;
    }
    return p;
}

Strateginya berlapis. Loop find('|') memotong baris menjadi bagian-bagian; jumlahnya harus tepat empat, tidak lebih dan tidak kurang, sehingga baris yang kehilangan pemisah langsung ditolak dan dinyatakan sebagai std::nullopt. Harga dan stok diurai lewat parseAngka yang sudah teruji sejak Bagian 1 — baris dengan harga abc tertahan di sini, bukan saat data sudah masuk inventaris. Terakhir, produk hasil uraian diperiksa ulang dengan sah(): berkas yang dipegang pengguna bisa berisi -50 pada kolom stok, dan function ini menolaknya sama seperti tambah. Semua penolakan mengembalikan std::nullopt karena pemanggilnya yang tahu konteks — muat menambahkan nomor baris ke pesan galatnya.

Dua function di atasnya mengurus berkas. Perhatikan simetri kegagalannya:

cpp src/inventaris.cpp (potongan: simpanKeFile dan muatDariFile)
std::expected<void, Galat> simpanKeFile(const Inventaris& inv,
                                        const std::string& namaBerkas) {
    std::ofstream keluar(namaBerkas);
    if (!keluar) {
        return std::unexpected(Galat{JenisGalat::BerkasGagal, namaBerkas});
    }
    for (const Produk& p : inv.daftar()) {
        keluar << p.kode << '|' << p.nama << '|' << p.harga << '|' << p.stok
               << '\n';
    }
    return {};
}

std::expected<Inventaris, Galat> muatDariFile(const std::string& namaBerkas) {
    std::ifstream masuk(namaBerkas);
    if (!masuk) {
        return std::unexpected(Galat{JenisGalat::BerkasGagal, namaBerkas});
    }
    Inventaris inv;
    std::string baris;
    int nomorBaris = 0;
    while (std::getline(masuk, baris)) {
        ++nomorBaris;
        if (baris.empty()) {
            continue;   // baris kosong (mis. di akhir berkas) diabaikan
        }
        auto p = uraiBarisProduk(baris);
        if (!p) {
            return std::unexpected(
                Galat{JenisGalat::BarisRusak,
                      std::to_string(nomorBaris) + ": " + baris});
        }
        auto hasil = inv.tambah(*p);
        if (!hasil) {
            return std::unexpected(hasil.error());
        }
    }
    return inv;   // data lama hanya diganti bila seluruh berkas berhasil dibaca
}

simpanKeFile membuka std::ofstream dan memeriksa keberhasilannya sebelum menulis satu byte pun; bila gagal — disk penuh, jalur tidak ada, izin ditolak — jenis BerkasGagal membawa nama berkasnya ke pemanggil. muatDariFile lebih menarik: ia membangun Inventaris baru dan hanya mengembalikannya bila seluruh berkas berhasil dibaca. Satu baris rusak di tengah berkas menggagalkan semuanya, dengan pesan BarisRusak yang menyebut nomor baris dan isinya. Pola all-or-nothing ini disengaja: inventaris yang setengah termuat lebih berbahaya daripada inventaris yang tidak termuat sama sekali, karena pengguna bisa salah mengira datanya lengkap. Di main, penerimaannya pun menjadi atomik: inv = std::move(*hasil); mengganti isi memori hanya setelah muat sukses.

Mencoba Simpan dan Muat #

Kompilasi, lalu jalankan sesi yang menyimpan, menambah produk baru, dan memuat kembali — pengguna seharusnya melihat data kembali ke kondisi saat tersimpan:

bash
g++ -std=c++23 -Wall -Wextra inventaris.cpp -o inventaris
printf 'tambah P001 Kopi 15000 20\ntambah P002 Gula 12000 5\nsimpan inv.txt\ntambah P009 Teh 3000 7\ndaftar\nmuat inv.txt\ndaftar\nmuat tidak-ada.txt\n' | ./inventaris

Output yang Dihasilkan:

text
ditambahkan: P001 (Kopi)
ditambahkan: P002 (Gula)
tersimpan: inv.txt (2 produk)
ditambahkan: P009 (Teh)
KODE  NAMA                HARGA  STOK
P001  Kopi                15000    20
P002  Gula                12000     5
P009  Teh                  3000     7
dimuat: inv.txt (2 produk)
KODE  NAMA                HARGA  STOK
P001  Kopi                15000    20
P002  Gula                12000     5
galat: berkas tidak dapat dibuka: tidak-ada.txt
selesai.

Alurnya persis seperti dirancang: dua produk tersimpan, Teh yang ditambahkan sesudahnya ikut hilang saat muat mengganti isi memori dengan isi berkas — bukti semantik penggantian, bukan penambahan. Percobaan memuat berkas yang tidak ada ditolak dengan BerkasGagal tanpa menjatuhkan program. Sekarang kita lihat isi berkasnya sendiri:

bash
cat inv.txt

Output yang Dihasilkan:

text
P001|Kopi|15000|20
P002|Gula|12000|5

Dua baris pendek itulah seluruh format kita — dan karena berkasnya teks biasa, perbaikan data darurat bisa dilakukan editor teks, dengan risiko yang sudah ditangani pemeriksaan di muatDariFile.

Inventaris kini lengkap: model data, CRUD, pencarian, dan persistensi. Tetapi “lengkap” adalah klaim — dan klaim tanpa bukti otomatis rapuh. Bagian 5 mengunci seluruh perilaku aplikasi ini dengan test harness yang memeriksa 51 pemeriksaan dalam sekali jalan.

Rujukan resmi: std::ofstream (cppreference) , std::ifstream (cppreference) .