Selain exception, C++ menyediakan pendekatan berbasis nilai (value-based) yang melaporkan kegagalan lewat tipe balik fungsi, yaitu std::optional dan std::variant sejak C++17 serta std::expected sejak C++23. Kegagalan menjadi bagian dari nilai yang dikembalikan, sehingga terlihat pada tanda tangan fungsi (signature) dan dapat diperiksa tanpa mekanisme pelemparan. Topik ini membahas ketiga tipe tersebut beserta situasi yang paling cocok untuk masing-masing, dengan contoh pembacaan konfigurasi dan konversi teks ke angka.
std::optional untuk kehadiran yang tidak pasti#
std::optional<T> adalah pembungkus yang memuat satu nilai bertipe T atau tidak memuat apa-apa, dengan keadaan kosong dinyatakan oleh std::nullopt. Tipe ini cocok untuk fungsi yang hasilnya memang mungkin tidak ada, misalnya pencarian yang tidak menemukan datanya atau pembacaan konfigurasi yang kuncinya belum diisi. Pemanggil memeriksa keberadaan nilai lewat konversi ke bool atau fungsi anggota has_value(), membacanya lewat operator* atau value(), dan menetapkan nilai cadangan lewat value_or().
Program berikut membaca konfigurasi sederhana berupa pasangan kunci dan teks, lalu mengubah nilainya menjadi bilangan bulat.
#include <iostream>
#include <map>
#include <optional>
#include <string>
// membaca satu pengaturan bilangan bulat; nullopt berarti tidak tersedia
std::optional<int> bacaInt(const std::map<std::string, std::string>& konfigurasi,
const std::string& kunci) {
auto it = konfigurasi.find(kunci);
if (it == konfigurasi.end()) {
return std::nullopt;
}
try {
return std::stoi(it->second);
} catch (const std::exception&) {
return std::nullopt;
}
}
int main() {
std::map<std::string, std::string> konfigurasi{
{"batas", "80"}, {"nama", "lab"}};
if (auto batas = bacaInt(konfigurasi, "batas")) {
std::cout << "batas = " << *batas << "\n";
} else {
std::cout << "batas tidak tersedia\n";
}
if (auto rasio = bacaInt(konfigurasi, "rasio")) {
std::cout << "rasio = " << *rasio << "\n";
} else {
std::cout << "rasio tidak tersedia\n";
}
if (auto nama = bacaInt(konfigurasi, "nama")) {
std::cout << "nama = " << *nama << "\n";
} else {
std::cout << "nama bukan angka\n";
}
return 0;
}batas = 80
rasio tidak tersedia
nama bukan angkaFungsi bacaInt mengembalikan std::nullopt dalam dua keadaan yang berbeda, yaitu kunci yang tidak ada di dalam peta dan teks yang bukan angka. Kedua keadaan itu disatukan menjadi satu jawaban yang sama, yaitu tidak ada nilai. Penyatuan ini adalah kekuatan sekaligus batasan std::optional, karena alasan ketiadaan tidak dapat dibawa bersama nilainya. Apabila alasan kegagalan perlu disampaikan kepada pemanggil, std::variant atau std::expected menjadi pilihan yang lebih tepat.
Dalam pemakaian sehari-hari, std::optional paling tepat untuk ketiadaan yang normal dan dapat diantisipasi, seperti kolom konfigurasi yang boleh tidak diisi. Tipe ini tidak tepat untuk keadaan yang menurut desain tidak boleh terjadi, karena kekosongannya akan diperlakukan seperti nilai sah alih-alih dilaporkan sebagai error. Ukuran penyimpanannya juga sedikit lebih besar daripada tipenya karena tambahan penanda kekosongan, sehingga pembungkus ini layak ditambahkan ketika ketiadaan memang bagian dari makna hasilnya.
std::variant untuk hasil dengan beberapa alternatif#
std::variant<A, B> memuat tepat satu nilai dari beberapa tipe alternatif dan menjaga tipe yang sedang aktif tetap terlacak. Untuk penanganan error, alternatif pertama biasanya berupa hasil yang sah, sedangkan alternatif kedua berupa tipe yang membawa pesan kegagalan, misalnya std::string. Akses ke isinya dilakukan lewat std::get atau lewat std::visit. Fungsi std::get melempar std::bad_variant_access bila tipe yang diminta tidak sedang aktif, sedangkan std::visit menjalankan fungsi yang diberikan terhadap nilai yang sedang aktif.
Program berikut mengembalikan hasil konversi teks ke angka dalam std::variant<int, std::string> dan menanganinya dengan satu lambda generik.
#include <iostream>
#include <string>
#include <variant>
// hasil konversi: int bila berhasil, std::string berisi pesan bila gagal
std::variant<int, std::string> keInt(const std::string& teks) {
if (teks.empty()) {
return std::string("gagal: teks kosong");
}
int hasil = 0;
for (char c : teks) {
if (c < '0' || c > '9') {
return std::string("gagal: karakter non-digit ") + c;
}
hasil = hasil * 10 + (c - '0');
}
return hasil;
}
int main() {
const std::string daftar[] = {"24", "10x", ""};
for (const std::string& teks : daftar) {
std::variant<int, std::string> hasil = keInt(teks);
// satu lambda menangani kedua alternatif tanpa pemeriksaan tipe manual
std::visit([&teks](const auto& nilai) {
std::cout << '"' << teks << "\" -> " << nilai << "\n";
}, hasil);
}
return 0;
}"24" -> 24
"10x" -> gagal: karakter non-digit x
"" -> gagal: teks kosongLambda yang dikirim ke std::visit ditulis dengan parameter auto, sehingga badan yang sama menangani alternatif int maupun std::string tanpa pemeriksaan tipe manual. Apabila keduanya membutuhkan perlakuan yang berbeda, pemeriksaan dapat dilakukan lewat std::holds_alternative<T> sebelum isi variant dibaca. Pola ini memindahkan keputusan ke titik pembacaan hasil, bukan ke titik kegagalan, sehingga fungsi keInt dapat ditulis tanpa satu pun pernyataan throw.
Alternatif pada std::variant tidak terbatas pada dua tipe, dan tipe errornya tidak harus std::string. Pada proyek yang lebih besar, pesan error lazim diganti dengan tipe khusus berisi kode dan konteks kegagalannya, sehingga pemeriksaannya dapat diuji tanpa membandingkan teks. Posisi alternatif yang sedang aktif dapat diperiksa lewat fungsi anggota index(), sedangkan akses aman tanpa pelemparan disediakan std::get_if yang mengembalikan pointer.
std::expected untuk nilai atau error#
std::expected<T, E> dari C++23 mempertegas pola di atas dengan memisahkan kedua maknanya secara eksplisit: satu sisi memuat nilai bertipe T, sisi lainnya memuat error bertipe E. Tipe balik fungsi langsung menyatakan bentuk kegagalannya, misalnya std::expected<double, std::string>, dan nilai errornya dibaca lewat fungsi anggota error(). Dibandingkan dengan std::variant, tipe ini menyediakan nama dan fungsi anggota yang khusus untuk penanganan error, sehingga maksudnya tidak perlu ditebak dari urutan alternatifnya.
Program berikut menggunakan std::expected untuk pembagian aman. Karena std::expected baru tersedia sejak C++23, kompilasinya memerlukan opsi -std=c++23.
// tidak-dijalankan (butuh -std=c++23; perintah build ada di bawah)
#include <expected>
#include <iostream>
#include <string>
// tipe balik memuat nilai hasil atau pesan error, bukan salah satunya saja
std::expected<double, std::string> bagi(double pembilang, double pembagi) {
if (pembagi == 0.0) {
return std::unexpected(std::string("pembagi bernilai nol"));
}
return pembilang / pembagi;
}
int main() {
auto sukses = bagi(90, 6);
if (sukses) {
std::cout << "90 / 6 = " << *sukses << "\n";
}
auto gagal = bagi(90, 0);
if (!gagal) {
std::cout << "90 / 0 ditolak: " << gagal.error() << "\n";
}
return 0;
}# kompilasi dengan standar C++23
g++ -std=c++23 main.cpp -o main
# jalankan program
./main90 / 6 = 15
90 / 0 ditolak: pembagi bernilai nolPemeriksaan if (sukses) dan if (!gagal) memanfaatkan konversi ke bool yang bernilai benar ketika nilai hasilnya tersedia. Sisi error diisi lewat std::unexpected saat pembuatan objek, dan isinya dibaca lewat error(). Dengan tipe balik yang eksplisit, pembacaan nilai hasil selalu merupakan langkah yang disengaja, sehingga kegagalan lebih sulit terlewat dibandingkan kode status biasa.
Untuk rangkaian pemanggilan yang berantai, std::expected menyediakan fungsi anggota and_then, or_else, dan transform yang menyusun langkah demi langkah tanpa pemeriksaan manual di setiap titik. Pendekatan berbasis nilai ini membuat jalur berhasil dan jalur gagal tetap terlihat pada tipe baliknya. Sebagai catatan versi, ketiga fungsi anggota tersebut membutuhkan standar C++23 yang sama dengan std::expected itu sendiri.
Kesalahan umum#
- Membaca isi
std::optionalyang kosong, misalnya memakai*hasiltanpa pemeriksaan. Perilakunya tidak terdefinisi karena tidak ada nilai di dalamnya. Perbaikannya dengan memeriksa lewat konversi kebool, atau menggunakanvalue_or()yang menyediakan nilai cadangan. - Memanggil
std::get<T>padastd::variantdengan tipe yang tidak sedang aktif. Pemanggilan tersebut melemparstd::bad_variant_accessdan memutus alur program. Perbaikannya dengan memeriksastd::holds_alternativeterlebih dahulu atau menggunakanstd::visit. - Mengabaikan sisi error pada
std::expected. Nilai error yang tidak pernah dibaca menyamarkan kegagalan seolah-olah semuanya berhasil. Perbaikannya dengan memeriksa keberadaan nilai hasil sebelum isinya digunakan.
Ringkasan#
std::optional<T>menyatakan nilai yang mungkin absen, denganstd::nulloptsebagai penanda ketiadaan danvalue_or()sebagai nilai cadangan.std::variant<A, B>menyatakan hasil dengan beberapa alternatif, danstd::visitmenanganinya lewat lambda yang diterapkan pada tipe yang sedang aktif.std::expected<T, E>dari C++23 memisahkan nilai dan error secara eksplisit pada tipe balik, sehingga bentuk kegagalannya terbaca langsung dari deklarasi fungsi.
Referensi#
- std::optional — cppreference — dokumentasi lengkap optional beserta fungsi anggotanya.
- std::variant — cppreference — dokumentasi variant dan perilaku aksesnya.
- std::visit — cppreference — mekanisme penerapan fungsi pada alternatif yang sedang aktif.
- std::expected — cppreference — dokumentasi expected dari C++23 beserta contohnya.