Chapter sebelumnya berakhir dengan dua luka pada unscoped enum: nama enumerator yang bocor ke scope sekitar dan konversi implisit ke int yang lolos tanpa suara. C++11 memperkenalkan obatnya dengan kata kunci tambahan class — bentuk yang disebut scoped enum atau enum class. Namanya tetap menunjuk deretan konstanta bernama, tetapi compiler kini menjaga batas-batasnya dengan ketat. Pada Chapter ini kita menulis enum class pertama, membuktikan bahwa enumerator dua enum berbeda tidak lagi bertabrakan, mengeluarkan nilai numeriknya secara eksplisit lewat static_cast, dan menetapkan underlying type sendiri untuk menghemat memori.

Sintaks enum class dan Scope Tertutup #

Perbedaan sintaksnya kecil: tambahkan class setelah enum, lalu setiap penggunaan enumerator harus melewati nama enum-nya dengan operator :: — misalnya Status::Menunggu. Aturan :: ini bukan sekadar formalitas; ia menegaskan bahwa enumerator hidup di dalam scope enum, bukan di scope sekitarnya.

cpp
#include <iostream>
#include <cstdint>

// Scoped enum: enumerator hanya hidup di dalam scope Status
enum class Status {
    Sukses,
    Gagal,
    Menunggu
};

// Underlying type eksplisit: hemat memori untuk enum yang dibuat jutaan kali
enum class Level : uint8_t {
    Rendah,
    Sedang,
    Tinggi
};

void proses(Status s) {
    switch (s) {
    case Status::Sukses:
        std::cout << "Proses selesai dengan sukses\n";
        break;
    case Status::Gagal:
        std::cout << "Proses berhenti karena gagal\n";
        break;
    case Status::Menunggu:
        std::cout << "Proses masih menunggu\n";
        break;
    }
}

int main() {
    Status kondisi = Status::Menunggu;
    proses(kondisi);

    // Tidak ada konversi implisit: harus eksplisit lewat static_cast
    std::cout << "Nomor status: " << static_cast<int>(kondisi) << "\n";

    std::cout << "sizeof(Status) = " << sizeof(Status) << " byte\n";
    std::cout << "sizeof(Level)  = " << sizeof(Level) << " byte\n";

    return 0;
}

Output yang Dihasilkan:

text
Proses masih menunggu
Nomor status: 2
sizeof(Status) = 4 byte
sizeof(Level)  = 1 byte

Mari kita telaah apa yang terjadi pada baris-baris kode di atas:

  1. Baris 5-9 (enum class Status { ... }): definisi scoped enum dengan tiga enumerator bernilai implisit 0, 1, 2 — aturan penomorannya sama seperti unscoped enum.
  2. Baris 12-16 (enum class Level : uint8_t): bentuk dengan underlying type eksplisit. Bagian : uint8_t menyatakan bahwa enum ini diwakili uint8_t (unsigned 1 byte), bukan int.
  3. Baris 18-30 (void proses(Status s)): switch pada enum class bekerja wajar; tiap label case wajib menulis Status:: lengkap. Karena semua enumerator tercakup, tidak ada peringatan compiler tentang case yang terlewat.
  4. Baris 33-34: variabel dan parameter bertipe Status hanya boleh berisi enumerator Status — tidak ada angka lain yang bisa menyusup.
  5. Baris 37-40: nilai numerik dikeluarkan dengan static_cast<int>(kondisi), dan sizeof memperlihatkan perbedaan ukuran kedua enum.

Bukti scope tertutupnya paling terasa saat dua enum punya enumerator bernama sama. Kode berikut yang tadinya gagal dikompilasi pada unscoped enum kini sah dan bebas konflik:

cpp
enum class Warna { Merah, Hijau, Biru };
enum class Lampu { Merah, Kuning, Hijau };   // Merah di sini tidak bertabrakan

Warna kertas  = Warna::Merah;
Lampu lampu   = Lampu::Merah;                // dua konstanta, dua makna, tanpa konflik

Tidak Ada Konversi Implisit #

Scoped enum tidak lagi menyamar menjadi int. Menugaskan nilainya ke variabel int langsung ditolak compiler:

cpp
Status s = Status::Gagal;
int n = s;   // ditolak compiler

Pesan kesalahannya menjelaskan dengan gamblang:

text
error: cannot convert 'Status' to 'int' in initialization

Jika kita memang butuh nilai numeriknya — untuk dicetak, disimpan ke file, atau dikirim lewat protokol — niat itu harus dituliskan eksplisit dengan static_cast<int>(...), operator cast yang sudah kita kenal pada Chapter Konversi Tipe Data. Perbandingan antar enumerator tetap wajar (s == Status::Gagal sah karena satu tipe), tetapi s == 1 kini ditolak: compiler tidak lagi membiarkan angka mentah menyamar sebagai status. Arah sebaliknya, static_cast<Status>(2), juga tersedia untuk kasus membaca data dari file atau jaringan — gunakan dengan sadar, karena nilai 2 di luar daftar enumerator tidak dicek secara runtime.

Menentukan Underlying Type #

Secara bawaan, compiler memilih int sebagai penyimpan enum class, sehingga sizeof(Status) bernilai 4 byte pada platform ini. Ketika enum dibuat dalam jumlah besar — jutaan objek struct yang masing-masing membawa satu status — 4 byte per field terasa boros. Deklarasi enum class Level : uint8_t memaksa penyimpanannya satu byte; output di atas membuktikan sizeof(Level) turun menjadi 1 byte. Tipe yang dipilih haruslah tipe integral (int, unsigned char/uint8_t, long long, dan seterusnya), dan nilai semua enumerator harus muat di dalamnya.

cpp
enum class Status : uint8_t { Sukses, Gagal, Menunggu };   // 1 byte
enum class Opsi : int { A, B, C };                         // 4 byte (eksplisit)

Kombinasi yang paling sering kita temui di kode embedded dan game engine adalah enum class ... : uint8_t untuk flag kecil, dan underlying type bawaan (int) untuk sisanya. Detail ukuran tipe integral ini sudah kita bahas pada Section Variabel dan Tipe Data; di sini kita hanya memilihnya untuk enum.

Mengapa enum class Lebih Disarankan #

Menyusun perbandingan dua bentuk enum di atas, keunggulan enum class dapat kita petakan secara langsung:

  • Nama aman: enumerator tidak memenuhi scope sekitar, sehingga dua enum boleh punya nama enumerator yang sama.
  • Tanpa konversi terselubung: tidak ada jalur diam-diam ke/dari int, sehingga pembandingan dan penugasan lintas makna tertangkap compiler.
  • Ukuran terkendali: underlying type bisa ditentukan, sesuatu yang juga dimungkinkan pada unscoped enum tetapi jarang dipakai.
  • Maksud terbaca: Status::Gagal menyebut asal-usulnya, sementara Gagal yang melayang bebas bisa berasal dari enum mana pun.

Mengapa enum class tetap tidak menggantikan unscoped enum sepenuhnya? Jawabannya kompatibilitas: kode C dan kode C++ lama bergantung pada perilaku lama, dan beberapa kasus (misalnya bitmask sederhana) masih nyaman dengan konversi implisit. Untuk kode baru yang kita tulis sekarang, pilih enum class kecuali ada alasan kuat untuk sebaliknya.

Referensi resmi tersedia pada halaman enumeration dan static_cast di cppreference. Selanjutnya kita membahas tipe komposit terakhir yang paling sering disalahpahami: union, satu alamat memori yang dipakai bergantian oleh beberapa member.