Sebelum C++17, menandai “fungsi ini bisa gagal menghasilkan nilai” selalu memakai trik yang sama: kembalikan nilai penanda seperti -1, nullptr, atau string kosong, lalu berharap semua pemanggil ingat memeriksanya. Trik itu rapuh — lupa satu pemeriksaan, nilai penanda ikut dihitung seperti nilai sah. std::optional<T> menggantikan trik itu dengan tipe yang dinyatakan secara eksplisit: objeknya berisi nilai T atau benar-benar kosong, dan compiler memaksa kita berurusan dengan kedua kemungkinan itu. Chapter ini membandingkan gaya lama dan gaya optional secara berdampingan, lalu merangkum kapan optional menjadi pilihan yang tepat.

Sentinel vs optional: Masalah yang Dipecahkan #

Gaya sentinel memiliki cacat mendasar: nilai penanda -1 adalah int biasa, sehingga cariSentinel(teks, 'x') * 2 tetap dikompilasi tanpa keluhan — dan menghasilkan -2, angka yang sama sekali tidak bermakna. Dengan std::optional<int>, tidak ada aritmetika langsung: untuk memakai nilainya kita wajib melewati pemeriksaan (if, value_or, atau *), sehingga jalur kegagalan tidak mungkin terselip diam-diam.

cpp
#include <iostream>   // std::cout
#include <optional>   // std::optional, std::nullopt
#include <string>     // std::string

// Cara lama: sentinel -1 berarti "tidak ditemukan"
int cariSentinel(const std::string& teks, char target) {
    for (std::size_t i = 0; i < teks.size(); ++i) {
        if (teks[i] == target) return static_cast<int>(i);
    }
    return -1;   // nilai khusus penanda "gagal"
}

// Cara modern: optional<int> berarti "posisi, ATAU tidak ada"
std::optional<int> cariOptional(const std::string& teks, char target) {
    for (std::size_t i = 0; i < teks.size(); ++i) {
        if (teks[i] == target) return static_cast<int>(i);
    }
    return std::nullopt;   // tanpa nilai, tanpa trik angka
}

struct Kupon {
    std::string kode;
    int diskon;   // persen
};

// optional juga bisa menampung struct: data ATAU kegagalan
std::optional<Kupon> cariKupon(const std::string& kode) {
    if (kode == "HEMAT20") return Kupon{"HEMAT20", 20};
    return std::nullopt;
}

int main() {
    std::string kalimat = "standard library";

    // --- gaya sentinel ---
    std::cout << "Sentinel 'd'   : " << cariSentinel(kalimat, 'd') << "\n";
    std::cout << "Sentinel 'x'   : " << cariSentinel(kalimat, 'x') << "\n";

    // --- gaya optional ---
    std::optional<int> posisi = cariOptional(kalimat, 'd');
    if (posisi.has_value()) {   // bentuk ringkas: if (posisi)
        std::cout << "Optional 'd'   : " << *posisi << "\n";
    }

    std::optional<int> kosong = cariOptional(kalimat, 'x');
    std::cout << "Optional 'x' ada nilai? "
              << (kosong ? "ya" : "tidak") << "\n";

    // value_or: nilai cadangan bila kosong, tanpa pemeriksaan eksplisit
    std::cout << "value_or 'x'   : " << kosong.value_or(0) << "\n";
    std::cout << "value_or 'd'   : " << posisi.value_or(0) << "\n";

    // --- optional berisi struct ---
    std::optional<Kupon> kupon = cariKupon("HEMAT20");
    if (kupon) {
        std::cout << "Kupon          : " << kupon->kode
                  << " (-" << kupon->diskon << "%)\n";
    }
    std::optional<Kupon> gagal = cariKupon("SALAH123");
    std::cout << "Kupon SALAH123 : "
              << (gagal.has_value() ? "ada" : "tidak ada") << "\n";

    return 0;
}

Output yang Dihasilkan:

text
Sentinel 'd'   : 4
Sentinel 'x'   : -1
Optional 'd'   : 4
Optional 'x' ada nilai? tidak
value_or 'x'   : 0
value_or 'd'   : 4
Kupon          : HEMAT20 (-20%)
Kupon SALAH123 : tidak ada

Mari kita bedah:

  1. Baris 10 (return -1;): inilah gaya lama. -1 bukan posisi yang sah — ia kode bermimpi menjadi pesan kegagalan, dan tidak ada yang mencegah pemanggil memperlakukannya sebagai posisi.
  2. Baris 18 (return std::nullopt;): std::nullopt adalah konstanta “tanpa nilai” milik optional. Tipe kembalian std::optional<int> sudah menceritakan kontraknya di tanda tangan fungsi: hasilnya int atau tidak ada.
  3. Baris 41-42 (has_value dan *posisi): pemeriksaan keberadaan memakai has_value(), dan bentuk ringkasnya cukup if (posisi) karena optional bisa dikonversi ke bool. Operator * mengambil nilai di dalamnya — sah hanya setelah kita yakin berisi.
  4. Baris 46-47: percobaan 'x' menghasilkan optional kosong; pencetakan kosong ? "ya" : "tidak" kembali memakai konversi ke bool.
  5. Baris 50-51 (value_or): value_or(0) berarti “ambil nilai, atau 0 bila kosong” — satu panggilan untuk pola nilai cadangan yang sehari-hari kita pakai (misalnya angka default). Perhatikan 'x' menghasilkan 0 sedangkan 'd' tetap 4.
  6. Baris 56-57 (operator->): untuk optional berisi struct, kupon->kode mengakses anggota tanpa menulis (*kupon).kode. Semantiknya sama dengan pointer, tetapi objeknya disimpan di dalam optional, bukan di heap.
  7. Baris 59-61: kode kupon yang tidak terdaftar menghasilkan nullopt, dan pemanggilnya memeriksa sebelum menyentuh kode atau diskon — pola pemakaian yang aman dari salah baca.

value(), value_or(), dan operator* #

std::optional menyediakan tiga pintu menuju isinya, masing-masing dengan semantik berbeda:

CaraSaat berisiSaat kosong
if (opt) { *opt }mengembalikan nilai* pada optional kosong = undefined behavior
opt.value()mengembalikan nilaimelempar exception std::bad_optional_access
opt.value_or(x)mengembalikan nilaimengembalikan x

Karena value() melempar exception, pembahasannya baru tuntas setelah kita mengenal mekanisme try/catch pada Section Error Handling; untuk sekarang cukup diingat: bila jalur kosong memang biasa terjadi dan punya jawaban wajar, value_or adalah pilihan paling nyaman.

optional menjawab pertanyaan “ada atau tidak”. Pertanyaan berikutnya lebih kaya: “nilai ini berupa salah satu dari beberapa tipe yang mana?” — itulah std::variant, topik Chapter berikutnya.

Rujukan resmi: cppreference — std::optional , cppreference — optional::value_or .