Topik ini membahas cara merepresentasikan sekumpulan flag dalam satu nilai kecil menggunakan scoped enumeration (enum class), operasi bit, dan std::bitset. Pembahasan dimulai dari kelemahan enum klasik, dilanjutkan dengan penulisan operator bit untuk enum class, lalu penyimpanan flag pada std::bitset. Setelah membaca topik ini, pembaca dapat menyimpan delapan status benar-salah dalam satu byte, mengujinya dengan masking, dan memilih struktur data yang tepat untuk kebutuhan tersebut.
Masalah dengan enum klasik#
Enum klasik (unscoped enum) yang ditulis tanpa class memiliki dua kelemahan yang sering menimbulkan bug. Pertama, nama-nama nilainya bocor ke lingkup sekitarnya, sehingga dua enum yang sama-sama memiliki nilai Merah akan bertabrakan dan menyebabkan error definisi ganda. Kedua, nilainya berkonversi secara diam-diam menjadi bilangan bulat, sehingga perbandingan seperti warna == 2 tetap lolos kompilasi padahal maknanya tidak jelas dan mudah keliru.
enum class memperbaiki kedua hal tersebut. Nama nilainya wajib ditulis lengkap, misalnya Warna::Merah, dan nilainya tidak berkonversi ke tipe lain tanpa static_cast yang eksplisit. Underlying type-nya pun dapat dipilih secara jelas, misalnya unsigned char agar konstanta enum tetap berukuran satu byte. Kontrol inilah yang menjadikan enum class dasar yang layak untuk flag berbasis bit.
Scoped enum sebagai kumpulan flag#
Flag berbasis bit menyimpan setiap status pada satu posisi bit, sehingga nilai-nilainya dipilih sebagai pangkat dua, yaitu 1 << 0, 1 << 1, dan seterusnya. Menambahkan sebuah flag berarti melakukan operasi OR pada bit, sedangkan menguji sebuah flag berarti melakukan operasi AND terhadap bit yang dituju, teknik yang dikenal sebagai masking.
Compiler tidak menyediakan operator bit bawaan untuk enum class, karena tujuan utama enum class justru mencegah operasi angka yang tak terkendali. Operator tersebut ditulis sendiri dengan membongkar nilainya lewat static_cast, melakukan operasi bit pada underlying type-nya, lalu membungkusnya kembali. Program berikut menerapkan pola itu pada hak akses berkas.
#include <iostream>
enum class Izin : unsigned char {
Baca = 1 << 0,
Tulis = 1 << 1,
Eksekusi = 1 << 2,
};
constexpr Izin operator|(Izin a, Izin b) {
return static_cast<Izin>(
static_cast<unsigned char>(a) | static_cast<unsigned char>(b));
}
constexpr Izin operator&(Izin a, Izin b) {
return static_cast<Izin>(
static_cast<unsigned char>(a) & static_cast<unsigned char>(b));
}
int main() {
Izin izin_log = Izin::Baca | Izin::Tulis;
std::cout << std::boolalpha;
std::cout << "Nilai numerik: " << static_cast<int>(izin_log) << '\n';
std::cout << "Baca : " << ((izin_log & Izin::Baca) == Izin::Baca) << '\n';
std::cout << "Tulis : " << ((izin_log & Izin::Tulis) == Izin::Tulis) << '\n';
std::cout << "Eksekusi : " << ((izin_log & Izin::Eksekusi) == Izin::Eksekusi) << '\n';
return 0;
}Nilai numerik: 3
Baca : true
Tulis : true
Eksekusi : falseNilai numerik 3 berarti dua bit pertama menyala, yaitu pola biner 00000011. Ekspresi izin_log & Izin::Baca menyaring bit lain dan menyisakan bit Baca saja, sehingga hasilnya sama dengan Izin::Baca apabila flag itu terpasang. Penghapusan flag membutuhkan operator komplemen yang belum didefinisikan, sehingga polanya baru dapat ditunjukkan pada program berikutnya. Operator yang ditulis constexpr dihitung saat kompilasi tanpa biaya apa pun pada saat program berjalan.
Menghapus dan menambah kembali sebuah flag menggunakan pasangan operator yang sama. Menambah flag dilakukan dengan OR terhadap flag yang dituju, sedangkan menghapusnya dilakukan dengan AND terhadap komplemen flag tersebut, ditulis ~Izin::Tulis. Operator ~ juga perlu didefinisikan sendiri untuk enum class, dan penerapannya pada underlying type unsigned char membuat komplemennya bebas dari bit tanda. Program berikut memperluas pola di atas dengan operator~, fungsi bantu punya, serta urutan tambah dan hapus flag.
#include <iostream>
enum class Izin : unsigned char {
Baca = 1 << 0,
Tulis = 1 << 1,
Eksekusi = 1 << 2,
};
constexpr Izin operator|(Izin a, Izin b) {
return static_cast<Izin>(
static_cast<unsigned char>(a) | static_cast<unsigned char>(b));
}
constexpr Izin operator&(Izin a, Izin b) {
return static_cast<Izin>(
static_cast<unsigned char>(a) & static_cast<unsigned char>(b));
}
constexpr Izin operator~(Izin a) {
return static_cast<Izin>(~static_cast<unsigned char>(a));
}
constexpr bool punya(Izin status, Izin flag) {
return (status & flag) == flag;
}
int main() {
Izin izin_log = Izin::Baca | Izin::Tulis;
izin_log = izin_log | Izin::Eksekusi; // tambah flag
izin_log = izin_log & ~Izin::Tulis; // hapus flag
std::cout << std::boolalpha;
std::cout << "pola bit akhir : " << static_cast<int>(izin_log) << '\n';
std::cout << "punya Baca : " << punya(izin_log, Izin::Baca) << '\n';
std::cout << "punya Tulis : " << punya(izin_log, Izin::Tulis) << '\n';
std::cout << "punya Eksekusi : " << punya(izin_log, Izin::Eksekusi) << '\n';
return 0;
}pola bit akhir : 5
punya Baca : true
punya Tulis : false
punya Eksekusi : trueNilai akhir 5 berarti pola bit 00000101, yaitu Baca dan Eksekusi menyala sedangkan Tulis telah dipadamkan. Ekspresi izin_log & ~Izin::Tulis terlebih dahulu membalik seluruh bit Izin::Tulis, sehingga polanya menjadi 11111101, lalu menyaring bit lain sehingga hanya bit Tulis yang dipadamkan. Fungsi bantu punya membungkus pola perbandingan pada contoh sebelumnya, sehingga kode pembaca flag menjadi lebih singkat tanpa kehilangan artinya.
Kapasitas flag dan underlying type#
Jumlah flag yang dapat ditampung ditentukan oleh underlying type yang dipilih pada deklarasi enum. Setiap bit dari underlying type menampung satu flag, sehingga pilihannya menjadi keputusan kapasitas yang eksplisit, bukan hal yang dibiarkan compiler memutuskan sendiri.
| Underlying type | Ukuran | Kapasitas flag |
|---|---|---|
unsigned char | 1 byte | 8 |
unsigned short | 2 byte | 16 |
unsigned int | 4 byte | 32 |
unsigned long long | 8 byte | 64 |
Underlying type tanpa tanda dipilih karena pergeseran bit pada tipe bertanda yang mendekati batas atasnya berujung pada undefined behavior, kelompok masalah yang dibahas lebih lanjut pada topik undefined behavior. Tipe tanpa tanda juga membuat operator~ bekerja tanpa bit tanda yang ikut terbalik. Underlying type enum yang sudah ditulis dapat dibaca kembali dengan std::underlying_type_t<Izin>, trait yang berguna saat operator generik perlu mengetahui tipe kerjanya secara otomatis. Ukuran enum itu sendiri dapat dijaga dengan static_assert(sizeof(Izin) == 1), sehingga perubahan yang tanpa sengaja memperbesar struktur data langsung terdeteksi pada saat kompilasi.
Flag berbasis bit dengan std::bitset#
Apabila flag yang disimpan tidak memerlukan nama bermakna, std::bitset dari header <bitset> menyediakan wadah bit berukuran tetap tanpa perlu menulis operator apa pun. Ukurannya dinyatakan sebagai parameter template, misalnya std::bitset<8> untuk delapan bit, dan setiap bit diakses lewat indeks. Bit ke-0 berada di posisi paling kanan, sehingga pada string inisialisasi "00100101" bit yang menyala berada pada indeks 0, 2, dan 5.
#include <bitset>
#include <iostream>
int main() {
std::bitset<8> status("00100101");
std::cout << "Status awal : " << status << '\n';
std::cout << "Bit ke-0 menyala : " << status.test(0) << '\n';
status.set(7);
std::cout << "Setelah set(7) : " << status << '\n';
status.reset(2);
std::cout << "Setelah reset(2) : " << status << '\n';
status.flip();
std::cout << "Setelah flip : " << status << '\n';
std::cout << "Bit bernilai 1 : " << status.count() << '\n';
return 0;
}Status awal : 00100101
Bit ke-0 menyala : 1
Setelah set(7) : 10100101
Setelah reset(2) : 10100001
Setelah flip : 01011110
Bit bernilai 1 : 5Method test membaca satu bit, set menyalakan, reset memadamkan, flip membalik seluruh bit atau satu bit, dan count menghitung bit yang menyala. Method test melempar std::out_of_range apabila indeksnya melampaui ukuran bitset, sedangkan operator [] membaca tanpa pemeriksaan untuk kasus ketika indeks sudah pasti sah. Keluaran status ditampilkan sebagai rangkaian bit langsung oleh operator <<, sehingga tidak perlu konversi manual seperti pada enum.
Beberapa method lain melengkapi kebutuhan pengujian sehari-hari. Method any bernilai true apabila minimal satu bit menyala, none bernilai true apabila seluruhnya padam, dan all menuntut setiap bit menyala, sedangkan size melaporkan jumlah bit yang dialokasikan. Kombinasi count dan size juga cukup untuk menyatakan tingkat keterisian bitset tanpa perulangan manual.
Memilih antara enum dan bitset#
Keduanya menempati memori yang sama-sama ringkas, tetapi tujuannya berbeda. enum class dipilih apabila setiap flag punya arti khusus yang perlu disebut namanya, misalnya hak akses atau status modul, karena compiler menjamin hanya nilai yang sah yang dapat dipakai. std::bitset dipilih apabila yang dibutuhkan adalah deretan status seragam yang diakses lewat indeks, misalnya ketersediaan kursi pada 64 slot, karena ukurannya dapat diatur bebas dan operasinya sudah tersedia.
Gaya ketiga yang masih banyak ditemui adalah bitmask berbasis unsigned int dari kebiasaan C, yaitu menyimpan flag pada konstanta seperti 0x04 dan mengoperasikannya langsung. Gaya ini bekerja, tetapi tidak memberi perlindungan tipe apa pun, sehingga keliru mengirim flag dari konteks lain tetap lolos kompilasi. Pada kode baru, enum class untuk flag bernama dan bitset untuk flag generik memberi hasil yang sama dengan risiko yang jauh lebih kecil.
Ringkasan#
enum classmenutup nama nilai di dalam enum-nya dan melarang konversi diam-diam ke bilangan bulat.- Flag berbasis bit dipilih sebagai pangkat dua, ditambahkan dengan OR, dan diuji dengan AND terhadap bit yang dituju.
- Operator bit untuk
enum classditulis sendiri sebagaiconstexprdenganstatic_castpada underlying type-nya. std::bitset<N>menyimpan N bit dengan methodtest,set,reset,flip, dancounttanpa konversi manual.- Kapasitas flag mengikuti underlying type enum yang dipilih, dan komplemen dari
operator~dipakai untuk menghapus flag.
Referensi#
- Enumerations — cppreference.com — Aturan enum klasik dan scoped enum beserta underlying type-nya.
- std::bitset — cppreference.com — Operasi bitset berukuran tetap, termasuk test, set, dan count.
- Operator overloading — cppreference.com — Aturan menulis operator kustom seperti operator bit untuk enum.