Struct menyimpan semua member sekaligus; union adalah kebalikannya — semua member berbagi satu area memori yang sama, sehingga hanya satu member yang berlaku pada satu waktu. Fitur ini lahir dari era ketika setiap byte memori mahal: kalau sebuah variabel dijamin tidak pernah dipakai bersamaan dengan variabel lain, mengapa mengalokasikan tempat untuk keduanya? Union masih relevan hingga sekarang untuk menghemat memori, tetapi aturan mainnya paling ketat di antara tipe komposit yang kita bahas, karena salah baca member berarti menyentuh undefined behavior. Pada Chapter ini kita membedah cara kerja union, mengukur penghematannya, menerapkan pola tagged union yang aman, dan mengenal penerus modernnya: std::variant.
Union Menyimpan Satu Member pada Satu Waktu #
Deklarasi union mirip struct — kata kuncinya union — tetapi makna memoriannya berbeda total. Semua member mulai dari offset 0 (alamat yang sama), dan ukuran union adalah ukuran member terbesar ditambah aturan alignment. Menulis ke satu member mengganti isi memori yang sama, sehingga nilai member lain yang ditulis sebelumnya tidak berlaku lagi.
#include <cstdint>
#include <iostream>
// Union: semua member berbagi satu area memori
union Simpan {
int angka;
double desimal;
char teks[8];
};
// Alternatif "simpan semuanya" untuk pembandingan ukuran
struct SimpanSemua {
int angka;
double desimal;
char teks[8];
};
// Tagged union: enum class sebagai penanda member yang sedang aktif
enum class JenisData : uint8_t { Angka, Desimal };
struct DataBertanda {
JenisData jenis; // tag
Simpan isi;
};
void cetak(const DataBertanda& d) {
if (d.jenis == JenisData::Angka) {
std::cout << "Angka: " << d.isi.angka << "\n"; // member aktif — aman
} else {
std::cout << "Desimal: " << d.isi.desimal << "\n"; // member aktif — aman
}
}
int main() {
std::cout << "sizeof(int) = " << sizeof(int) << " byte\n";
std::cout << "sizeof(double) = " << sizeof(double) << " byte\n";
std::cout << "sizeof(Simpan::teks) = " << sizeof(Simpan::teks) << " byte\n";
std::cout << "sizeof(Simpan) = " << sizeof(Simpan) << " byte\n";
std::cout << "sizeof(SimpanSemua) = " << sizeof(SimpanSemua) << " byte\n";
DataBertanda a;
a.jenis = JenisData::Angka;
a.isi.angka = 42;
cetak(a);
DataBertanda b;
b.jenis = JenisData::Desimal;
b.isi.desimal = 3.5;
cetak(b);
return 0;
}Output yang Dihasilkan:
sizeof(int) = 4 byte
sizeof(double) = 8 byte
sizeof(Simpan::teks) = 8 byte
sizeof(Simpan) = 8 byte
sizeof(SimpanSemua) = 24 byte
Angka: 42
Desimal: 3.5Mari kita telaah apa yang terjadi pada baris-baris kode di atas:
- Baris 5-9 (
union Simpan { ... }): tiga member dengan ukuran 4, 8, dan 8 byte. Ukuran union diambil dari member terbesar, makasizeof(Simpan)= 8 byte — bukan jumlah ketiganya. - Baris 12-16 (
struct SimpanSemua): struct pembanding dengan member yang persis sama. Ukurannya 24 byte karena tiap member duduk di alamat sendiri (dengan padding dari Chapter Member Variables). - Baris 19 (
enum class JenisData : uint8_t { Angka, Desimal };): sebuah tag bertipe enum class dari Chapter sebelumnya, dipakai untuk mencatat member mana yang sedang aktif. - Baris 21-24 (
struct DataBertanda): pola tagged union — enum penanda berdampingan dengan union. Gabungan keduanya butuh 16 byte, jauh lebih hemat daripada menyimpan tiga data sekaligus. - Baris 26-32 (
void cetak): sebelum membaca member union, fungsi memeriksa tag terlebih dahulu. Membaca member yang sedang aktif itulah satu-satunya cara penggunaan union yang aman. - Baris 41-49: objek
adanbmasing-masing mengisi tag lalu member union-nya; pembacaan padacetakselalu mengikuti tag yang benar, sehingga hasilnya deterministik.
Angka 8 lawan 24 byte terlihat kecil, tetapi kalikan dengan satu juta elemen di dalam std::vector — hematnya 16 MB. Di sistem embedded dengan RAM puluhan kilobyte, perbedaan seperti ini menentukan bisa-tidaknya sebuah produk berjalan.
Membaca Member yang Tidak Aktif: Undefined Behavior #
Sekarang bagian paling penting. Standar C++ menetapkan: membaca member union yang bukan member aktif (member yang terakhir ditulisi) adalah undefined behavior — perilaku tidak terdefinisi yang sudah kita singgung pada Section pointer-dan-reference. Perhatikan kode bermasalah berikut; kita sengaja tidak menjalankannya sebagai contoh resmi:
union Simpan {
int angka;
double desimal;
};
int main() {
Simpan s;
s.desimal = 3.5; // member aktif sekarang: desimal
int x = s.angka; // BACA MEMBER TIDAK AKTIF — undefined behavior
std::cout << x; // tidak ada jaminan apa pun atas hasilnya
}Mengapa dilarang tegas? Member union menafsirkan byte-byte yang sama dengan aturan representasi masing-masing tipe. double 3.5 disusun dari 8 byte berformat floating-point; membacanya sebagai int berarti menafsirkan ulang byte yang sama dengan aturan integer — hasilnya tidak memiliki arti yang terdefinisi oleh standar. Compiler berhak mengoptimalkan dengan asumsi bahwa kita selalu membaca member aktif, sehingga hasilnya bisa berbeda antar flag optimasi, antar versi compiler, bahkan dihilangkan sekaligus. Dua poin praktisnya:
- Jangan berharap crash. Undefined behavior sering “berjalan tampak normal” pada mesin kita, lalu berhenti benar di mesin lain. Program tidak berjanji gagal keras, ia berjanji tidak memberi jaminan apa pun.
- Kecuali kasus khusus yang diizinkan standar. Standar mengizinkan sebagian kecil pengecualian — misalnya memeriksa bagian awal yang sama (common initial sequence) dari dua struct standard-layout di dalam union, atau memindahkan byte lewat
std::memcpy. Aturan-aturan itu teknis dan mudah salah; aman untuk semua kasus adalah pola tagged union.
Pola Tagged Union dan Anonymous Union #
Pola tagged union pada contoh utama sudah memperlihatkan resep lengkapnya: enum class sebagai tag, union sebagai penyimpan, dan setiap pembacaan didahului pemeriksaan tag. Pola ini mendasar bagi parser file, interpreter, dan protokol jaringan yang nilai datanya bergantung konteks. Turunannya yang ringkas adalah anonymous union — union tanpa nama tipe dan tanpa nama member yang dideklarasikan di dalam struct; member-nya “berpindah” langsung ke scope struct pembungkus sehingga aksesnya tanpa nama perantara:
#include <iostream>
#include <string>
// Anonymous union: member-nya "dipindahkan" langsung ke struct pembungkusnya
struct Pengukuran {
std::string nama;
union { // tanpa nama tipe dan tanpa nama member
int levelPersen;
double voltase;
};
};
int main() {
Pengukuran s;
s.nama = "Baterai-A";
s.voltase = 3.7;
std::cout << s.nama << ": " << s.voltase << " volt\n";
s.levelPersen = 85; // member aktif berganti — nilai voltase tidak berlaku lagi
std::cout << s.nama << ": level " << s.levelPersen << " persen\n";
return 0;
}Output yang Dihasilkan:
Baterai-A: 3.7 volt
Baterai-A: level 85 persenPerhatikan bahwa aksesnya s.voltase dan s.levelPersen — tidak ada perantara seperti s.nilai.voltase. Aturan member aktif tetap berlaku persis: baris s.levelPersen = 85; menjadikan levelPersen satu-satunya member yang sah dibaca, dan nilai voltase lama tidak berlaku lagi. Anonymous union praktis untuk struct kecil, tetapi tag-nya tetap harus kita kelola sendiri bila dua member tidak bisa dibedakan dari konteks.
Alternatif Modern: std::variant #
Sejak C++17, standard library menyediakan std::variant<A, B, C> — tipe yang menyimpan tepat satu nilai dari daftar tipe yang diberikan, lengkap dengan penghitungan tag, pemeriksaan tipe saat runtime, dan penanganan kesalahan jika kita salah membaca. Varian ini menggantikan tagged union manual pada sebagian besar kebutuhan kode modern, tanpa risiko undefined behavior:
#include <variant>
std::variant<int, double> data;
data = 42; // sekarang menyimpan int
int angka = std::get<int>(data); // membaca dengan pemeriksaan tipe
std::variant berada di ranah standard library, jadi pembahasannya menunggu kita di Chapter variant pada Section Standard Library. Untuk sekarang, cukup catat kaidah pemilihannya: butuh kontrol byte-per-byte atau berinteraksi dengan kode C → union; butuh keamanan dan kecepatan menulis kode → std::variant.
Referensi resmi tersedia pada halaman union
dan std::variant
di cppreference. Berikutnya kita memoles sisi keterbacaan: memberi nama yang bermakna pada struct dan enum lewat type alias dengan using.