Topik ini membahas std::variant dari header <variant>, yaitu union yang type-safe dan dapat menampung tepat satu dari beberapa tipe alternatif. Pembahasan mencakup kelemahan union klasik, cara membaca isi variant dengan aman, serta pola visitor menggunakan std::visit. Setelah membaca topik ini, pembaca dapat memodelkan data yang bentuknya bervariasi, misalnya sel berisi angka atau teks, tanpa mengandalkan konversi manual dan tanpa membuka celah undefined behavior.
Union klasik dan masalahnya#
Union klasik menyimpan beberapa member pada memori yang sama, sehingga hanya satu member yang berlaku pada satu waktu. Masalahnya, union tidak mencatat member mana yang sedang aktif. Pembacaan member yang tidak sedang aktif bersifat undefined behavior. Cara umum untuk mengatasinya adalah menambahkan tag manual berupa enum, dan penjagaan tag itu ditanggung programer sendiri. Kesalahan sekecil apa pun dalam penjagaan tersebut menghasilkan data yang salah secara diam-diam.
std::variant<A, B, C> menyelesaikan masalah itu dengan menyimpan tag secara otomatis di dalam tipe. Variant selalu berisi tepat satu alternatif, selalu menyimpan keterangan alternatif mana yang aktif, dan menolak akses yang tidak sesuai pada saat program berjalan. Satu penyimpanan memori yang ringkas tetap dipertahankan, sedangkan keandalan aksesnya setara dengan tipe C++ biasa.
Membaca isi variant#
Pemeriksaan isi variant dilakukan dengan std::holds_alternative<T>, yang mengembalikan true apabila alternatif yang aktif bertipe T. Pengambilan nilainya memakai std::get<T>. Member index() melaporkan posisi alternatif aktif menurut urutan parameter template, dimulai dari nol. Apabila std::get dipanggil dengan tipe yang tidak sedang aktif, variant melempar std::bad_variant_access, sehingga kesalahan terdeteksi dan tidak menjadi data rusak yang senyap.
#include <iostream>
#include <string>
#include <variant>
using Sel = std::variant<int, std::string>;
int main() {
Sel sel = 42;
std::cout << "indeks tipe: " << sel.index() << '\n';
sel = "kertas A4";
std::cout << "indeks tipe: " << sel.index() << '\n';
std::cout << std::boolalpha
<< "berisi string: " << std::holds_alternative<std::string>(sel) << '\n';
std::cout << "isi: " << std::get<std::string>(sel) << '\n';
try {
std::cout << std::get<int>(sel) << '\n';
} catch (const std::bad_variant_access& e) {
std::cout << "salah tipe: " << e.what() << '\n';
}
return 0;
}indeks tipe: 0
indeks tipe: 1
berisi string: true
isi: kertas A4
salah tipe: std::get: wrong index for variantPenugasan sel = "kertas A4" menghancurkan nilai lama yang bertipe int, lalu membangun nilai std::string pada memori yang sama. Perpindahan tipe seperti ini dikelola variant secara otomatis, termasuk pemanggilan constructor dan destructor yang benar, sesuatu yang tidak pernah dilakukan union klasik.
Alternatif pembacaan yang tidak melempar exception tersedia melalui std::get_if. Fungsi ini menerima pointer ke variant dan mengembalikan pointer const ke nilainya apabila tipenya cocok, atau nullptr apabila tidak, sehingga hasilnya diuji seperti membaca pointer biasa. Bentuk ini lazim dipakai pada logika yang alurnya sudah bercabang, karena pemeriksaan tipe dan pembacaan nilainya selesai dalam satu ekspresi tanpa blok try.
Pola visitor dengan std::visit#
Rantai holds_alternative yang panjang menjadi sulit dibaca apabila jumlah alternatif bertambah. Pola visitor menggantikannya: std::visit menerima objek callable dan menerapkannya pada alternatif yang sedang aktif. Compiler memeriksa bahwa visitor tersebut dapat memanggil semua alternatif, sehingga lupa menangani satu tipe pun langsung terdeteksi sebagai error kompilasi, bukan sebagai bug saat program berjalan.
Bentuk visitor yang paling lazim pada C++17 adalah pola Overloaded, yaitu struct yang mewarisi kumpulan lambda sekaligus. Deklarasi using Ts::operator()... menghadirkan semua operator() milik lambda di dalam struct, sedangkan deduction guide pada baris berikutnya membuat parameter template terisi otomatis. Dengan begitu, kumpulan lambda dapat diperlakukan sebagai satu visitor.
#include <iostream>
#include <string>
#include <variant>
// Sel data: kosong (monostate), angka, atau teks.
using Sel = std::variant<std::monostate, int, std::string>;
// Pola Overloaded: kumpulan lambda menjadi satu visitor.
template <class... Ts> struct Overloaded : Ts... { using Ts::operator()...; };
template <class... Ts> Overloaded(Ts...) -> Overloaded<Ts...>;
void cetak(const Sel& sel) {
std::visit(Overloaded{
[](std::monostate) { std::cout << "(kosong)"; },
[](int angka) { std::cout << "angka: " << angka; },
[](const std::string& teks) { std::cout << "teks: " << teks; },
}, sel);
}
int main() {
Sel baris[] = {42, std::string("kertas A4"), std::monostate{}, 7};
for (const Sel& sel : baris) {
cetak(sel);
std::cout << '\n';
}
std::cout << "indeks tipe aktif baris pertama: " << baris[0].index() << '\n';
return 0;
}angka: 42
teks: kertas A4
(kosong)
angka: 7
indeks tipe aktif baris pertama: 1Fungsi cetak di atas tidak perlu diubah ketika alternatif variant ditambah, karena compiler akan menuntut satu lambda baru dan menunjuk penanganan yang belum ada. Visitor juga boleh mengembalikan nilai, yaitu std::visit meneruskan hasil lambda yang terpilih kepada pemanggilnya, dan seluruh cabangnya wajib mengembalikan tipe yang sama. Selain lambda, visitor juga dapat berupa struct dengan operator() yang di-overload, pilihan yang tepat apabila logika per tipe sudah panjang.
Variant sebagai hasil fungsi#
Variant sering dipakai bukan hanya sebagai penyimpan data, tetapi juga sebagai tipe hasil fungsi yang bisa berhasil atau gagal. Fungsi konversi teks menjadi bilangan adalah contohnya, yaitu hasil yang benar berupa angka, sedangkan kegagalan perlu menyertakan alasannya. Mengembalikan std::variant<int, std::string> menjadikan kedua kemungkinan itu bagian dari tipe hasil, sehingga pemanggil tidak mungkin mengabaikannya tanpa sengaja. Pola ini menggantikan kebiasaan mengembalikan kode status yang mudah terlewat, dan compiler memaksa setiap pembacaan hasilnya untuk memilih tipe yang tepat.
#include <iostream>
#include <string>
#include <variant>
using Hasil = std::variant<int, std::string>;
Hasil konversi(const std::string& teks) {
try {
return std::stoi(teks);
} catch (const std::exception&) {
return std::string("bukan bilangan: " + teks);
}
}
int main() {
Hasil a = konversi("128");
Hasil b = konversi("tiga");
std::cout << "hasil pertama : " << std::get<int>(a) << '\n';
std::cout << "indeks kedua : " << b.index() << '\n';
std::cout << "pesan kedua : " << std::get<std::string>(b) << '\n';
const int* angka = std::get_if<int>(&a);
std::cout << "get_if a : " << (angka != nullptr ? *angka : -1) << '\n';
return 0;
}hasil pertama : 128
indeks kedua : 1
pesan kedua : bukan bilangan: tiga
get_if a : 128Fungsi konversi mengembalikan hasil std::stoi apabila teksnya sah, dan mengembalikan pesan berupa std::string apabila konversinya melempar error. Di sisi pemanggil, std::get<int> dan std::get<std::string> membaca masing-masing kemungkinan, sedangkan std::get_if menyediakan bentuk pengecekan tanpa exception. Ukuran memori variant ini mengikuti alternatif terbesarnya, yaitu std::string, sehingga membawa angka pun tetap menyediakan ruang untuk teks. Apabila kegagalan tidak memerlukan pesan sama sekali, bentuk hasil yang lebih sederhana dapat dipertimbangkan, sedangkan variant dipilih ketika alasan kegagalannya perlu dibawa ke pemanggil.
std::monostate dan batas pemakaian#
Variant yang pertama kalinya dibangun tanpa nilai awal memuat alternatif pertama pada urutan template. Apabila alternatif pertama tidak memiliki constructor tanpa parameter, misalnya std::string memilikinya tetapi kelas buatan sendiri belum tentu, variant tidak dapat dibangun kosong. std::monostate menyelesaikan hal ini dengan menjadi alternatif pertama yang menyatakan keadaan kosong, sebagaimana Sel pada program sebelumnya.
Pemilihan tipe perlu disesuaikan dengan bentuk masalahnya. Variant cocok untuk data yang benar-benar berganti bentuk, misalnya isi kolom yang bisa angka atau teks. Untuk kasus ada atau tidaknya satu tipe saja, std::optional lebih ringkas, sedangkan untuk pewarisan perilaku yang beragam, polymorphism tetap lebih sesuai. Variant tidak menambah ukuran memori selain ruang alternatif terbesarnya ditambah tag kecil, sehingga biayanya nyaris setara union klasik.
Ringkasan#
- Union klasik tidak mencatat member yang aktif, sedangkan
std::variantmenyimpan tag tipe secara otomatis. std::holds_alternative,std::get, danindex()membaca isi variant, dan akses yang salah melemparstd::bad_variant_access.std::visitmenerapkan visitor pada alternatif aktif dan diperiksa compiler agar semua alternatif tertangani.- Pola Overloaded menggabungkan kumpulan lambda menjadi satu visitor, dan
std::monostatemenyediakan alternatif berarti kosong. - Variant juga layak menjadi tipe hasil fungsi yang bisa berhasil atau gagal, dengan
std::get_ifsebagai akses tanpa exception.
Referensi#
- std::variant — cppreference.com — Aturan variant, akses member, dan penanganan errornya.
- std::visit — cppreference.com — Mekanisme visitor untuk variant dan kategori nilai hasilnya.
- Union declaration — cppreference.com — Union klasik sebagai pembanding dan batasan pembacaannya.