union sendiri tidak aman: tidak ada cara untuk mengetahui member mana yang sedang aktif. Pembaca kode maupun program itu sendiri tidak bisa membedakan apakah angka 42 di as_int adalah nilai int yang dimaksud atau sampah dari penulisan as_float sebelumnya. Untuk memakai union dengan aman, kita perlu mencatat member mana yang sedang aktif di tempat terpisah—biasanya dalam struct yang menggabungkan union dengan sebuah tag enum. Pola ini disebut tagged union dan adalah cara C klasik untuk memodelkan tipe varian (data yang bisa berupa salah satu dari beberapa tipe). Pada chapter ini kita akan membangun tagged union langkah demi langkah dan memakainya untuk polimorfisme manual.
Anatomi Tagged Union #
Tagged union terdiri dari tiga komponen: (1) enum yang mendefinisikan kemungkinan tipe (disebut tag); (2) union yang menyimpan data salah satu tipe; (3) struct yang menggabungkan tag dan union. Untuk membaca data, kita periksa tag dulu lalu memilih member union yang sesuai. Konvensi ini membuat program aman: bila kita menulis data sebagai as_int, tag kind di-set ke VAL_INT, dan pembaca selalu tahu harus memilih as_int.
Mekanisme kerja di balik layar: kompiler menyusun struct dengan member kind di satu offset dan union data di offset berikutnya (dengan padding bila perlu). Pengaksesan data melalui switch (kind) memilih member union yang tepat pada runtime. Tanpa tag, kita akan terpaksa menyimpan tipe dalam void * yang kehilangan type safety—pola yang umum di C tapi rentan bug.
Tagged Union Dasar untuk Nilai Varian #
Kita mulai dengan contoh klasik: sebuah Value yang bisa berupa int, float, atau string. Untuk setiap tipe, kita sediakan constructor yang mengisi tag dan data sekaligus.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/* Tag untuk menandai tipe nilai yang aktif */
typedef enum {
VAL_INT,
VAL_FLOAT,
VAL_STR
} ValKind;
/* Union menyimpan data sesungguhnya */
typedef union {
int as_int;
float as_float;
char as_str[32];
} ValData;
/* Tagged union: struct menggabungkan tag + union */
typedef struct {
ValKind kind;
ValData data;
} Value;
/* Konstruktor untuk tiap tipe */
Value make_int(int v) { return (Value){VAL_INT, {.as_int = v}}; }
Value make_float(float v){ return (Value){VAL_FLOAT, {.as_float = v}}; }
Value make_str(const char *s) {
Value v;
v.kind = VAL_STR;
strncpy(v.data.as_str, s, sizeof(v.data.as_str) - 1);
v.data.as_str[sizeof(v.data.as_str) - 1] = '\0';
return v;
}
/* Fungsi yang memproses Value sesuai tag */
void tampilkan(const Value *v) {
switch (v->kind) {
case VAL_INT:
printf("INT = %d\n", v->data.as_int);
break;
case VAL_FLOAT:
printf("FLOAT = %.4f\n", v->data.as_float);
break;
case VAL_STR:
printf("STR = \"%s\"\n", v->data.as_str);
break;
default:
printf("UNKNOWN kind=%d\n", v->kind);
}
}
int main(void) {
Value values[3];
values[0] = make_int(42);
values[1] = make_float(3.14159f);
values[2] = make_str("Halo tagged union");
for (int i = 0; i < 3; i++) {
tampilkan(&values[i]);
}
printf("\nsizeof(Value) = %zu byte\n", sizeof(Value));
printf(" (kind=%zu + data=%zu + padding)\n",
sizeof(ValKind), sizeof(ValData));
return 0;
}Output yang Dihasilkan:
INT = 42
FLOAT = 3.1416
STR = "Halo tagged union"
sizeof(Value) = 36 byte
(kind=4 + data=32 + padding)Mari kita telaah baris-baris kode di atas:
- Baris 6-10 (
typedef enum { VAL_INT, VAL_FLOAT, VAL_STR } ValKind;): tag bertipeenumyang menyatakan tipe nilai yang aktif. Tiga kemungkinan nilai enumerator mewakili tiga tipe data. - Baris 13-18 (
typedef union { int as_int; float as_float; char as_str[32]; } ValData;): union menyimpan data. Memberas_strberukuran 32 byte—member terbesar—sehingga ukuranValDatamenjadi 32 byte. - Baris 21-24 (
typedef struct { ValKind kind; ValData data; } Value;): tagged union. Memberkindmenandai tipe aktif, memberdatamenyimpan nilai. Inilah inti polanya: tag + data. - Baris 27-29 (
Value make_int(int v) { return (Value){VAL_INT, {.as_int = v}}; }): konstruktor untuk tipeint. Compound literal(Value){...}membuat instanceValuedengankinddandatasekaligus. Designated initializer{.as_int = v}memilih memberas_intdariValData. Karena constructor mengisi tag dan data secara konsisten, penerima nilaiValueselalu tahu tipe aktif. - Baris 33-39 (
Value make_str(const char *s)): untuk string, kita tidak bisa pakai compound literal sederhana karena arrayas_strperlustrncpydengan batas ukuran. Kode ini menyalin string dengan aman—sizeof(v.data.as_str) - 1memastikan tidak overflow, lalu null terminator dipasang manual karenastrncpytidak menambahkannya bila source lebih panjang dari batas. - Baris 42-58 (
void tampilkan(const Value *v)): fungsi ini membaca data sesuai tag.switch (v->kind)memilih memberunionyang benar. Tanpa tag, kita tidak akan tahu harus bacaas_int,as_float, atauas_str. Kasusdefaultmenangani nilaikindyang tidak dikenal—penting untuk robustness. - Baris 67-69 (
Value values[3]; values[0] = make_int(42); ...): array berisi tigaValuedengan tipe berbeda. Loop berikutnya memanggiltampilkanuntuk setiap elemen—pola yang sangat alami untuk data heterogen.
Polimorfisme Manual dengan Tagged Union #
Salah satu penggunaan paling kuat tagged union adalah polimorfisme manual: sebuah struct umum Shape bisa berupa lingkaran, kotak, atau segitiga, dan fungsi luas menghitung rumus yang berbeda untuk masing-masing.
#include <stdio.h>
#include <string.h>
/* Tipe bentukan: Shape yang bisa berupa lingkaran, kotak, atau segitiga */
typedef enum {
SHAPE_CIRCLE,
SHAPE_RECT,
SHAPE_TRIANGLE
} ShapeKind;
typedef struct {
double radius;
} Circle;
typedef struct {
double width;
double height;
} Rect;
typedef struct {
double base;
double height;
} Triangle;
typedef union {
Circle circle;
Rect rect;
Triangle triangle;
} ShapeData;
typedef struct {
ShapeKind kind;
ShapeData data;
} Shape;
/* Polimorfisme manual: luas dihitung berdasarkan kind */
double luas(const Shape *s) {
switch (s->kind) {
case SHAPE_CIRCLE:
return 3.14159265 * s->data.circle.radius * s->data.circle.radius;
case SHAPE_RECT:
return s->data.rect.width * s->data.rect.height;
case SHAPE_TRIANGLE:
return 0.5 * s->data.triangle.base * s->data.triangle.height;
default:
return 0.0;
}
}
const char *nama_shape(ShapeKind k) {
switch (k) {
case SHAPE_CIRCLE: return "Lingkaran";
case SHAPE_RECT: return "Kotak";
case SHAPE_TRIANGLE: return "Segitiga";
default: return "Tidak dikenal";
}
}
int main(void) {
Shape shapes[3];
shapes[0].kind = SHAPE_CIRCLE;
shapes[0].data.circle = (Circle){2.0};
shapes[1].kind = SHAPE_RECT;
shapes[1].data.rect = (Rect){3.0, 4.0};
shapes[2].kind = SHAPE_TRIANGLE;
shapes[2].data.triangle = (Triangle){6.0, 5.0};
double total = 0.0;
for (int i = 0; i < 3; i++) {
double a = luas(&shapes[i]);
printf("%-10s luas = %.2f\n", nama_shape(shapes[i].kind), a);
total += a;
}
printf("Total luas: %.2f\n", total);
return 0;
}Output yang Dihasilkan:
Lingkaran luas = 12.57
Kotak luas = 12.00
Segitiga luas = 15.00
Total luas: 39.57Pada contoh di atas, tiap jenis bentuk (lingkaran, kotak, segitiga) punya struct sendiri dengan member berbeda: Circle hanya butuh radius, Rect butuh width dan height, Triangle butuh base dan height. Ketiganya dimasukkan ke union ShapeData, dan struct Shape menggabungkan tag kind dengan data. Fungsi luas memilih rumus berdasarkan tag—pola yang disebut dispatch on tag. Inilah cara C melakukan polimorfisme tanpa virtual function table. Saat menambah bentuk baru (misalnya SHAPE_HEXAGON), kita cukup menambah konstanta enumerator baru, struct baru untuk data bentuk tersebut, dan kasus baru di switch fungsi luas. Compiler biasanya memancarkan warning -Wswitch bila ada konstanta enumerator yang belum ditangani, membantu kita menemukan kasus yang terlewat.