C tidak memiliki template seperti C++ atau generics seperti Java, tetapi kita tetap bisa menulis fungsi yang bekerja pada tipe apa pun dengan memanfaatkan void * (pointer tanpa tipe) dan callback comparator. Pendekatan inilah yang dipakai oleh qsort, bsearch, malloc, memcpy, dan banyak fungsi standar lainnya. Pada chapter ini kita akan membangun fungsi swap generik yang bekerja untuk int, double, dan struktur apa pun, lalu membangun print_array generik dengan callback format untuk memahami pola yang dipakai standar library.

Mengapa void * Bekerja untuk Semua Tipe #

void * adalah pointer yang tidak membawa informasi tipe data yang ditunjuk. Karena itu compiler mengizinkan kita mengonversi int *, double *, atau struct Foo * menjadi void * secara implisit. Fungsi generik menerima void *, lalu mengoper ukuran byte size_t sz dari pemanggil. Operasi pada memori dilakukan lewat memcpy, memcmp, atau casting ke char * untuk aritmatika byte.

c
#include <stdio.h>
#include <string.h>

/* Swap generik: menukar dua blok memori sebesar sz byte */
void swap(void *a, void *b, size_t sz) {
    /* gunakan buffer lokal untuk aman dari aliasing */
    char tmp[64];
    if (sz > sizeof(tmp)) return;          /* batasi demo */
    memcpy(tmp, a, sz);
    memcpy(a, b, sz);
    memcpy(b, tmp, sz);
}

int main(void) {
    int x = 10, y = 99;
    printf("sebelum: x=%d y=%d\n", x, y);
    swap(&x, &y, sizeof(int));
    printf("sesudah: x=%d y=%d\n", x, y);

    double d1 = 3.14, d2 = 2.71;
    printf("sebelum: d1=%.2f d2=%.2f\n", d1, d2);
    swap(&d1, &d2, sizeof(double));
    printf("sesudah: d1=%.2f d2=%.2f\n", d1, d2);

    return 0;
}

Output yang Dihasilkan:

text
sebelum: x=10 y=99
sesudah: x=99 y=10
sebelum: d1=3.14 d2=2.71
sesudah: d1=2.71 d2=3.14

Mari kita telaah baris-baris kode di atas:

  1. Baris 6 (void swap(void *a, void *b, size_t sz)): signature generik. Dua parameter void * menerima alamat objek apa pun. sz menentukan berapa byte yang ditukar—tanpa informasi ini, fungsi tidak tahu seberapa besar blok memori yang harus disalin.
  2. Baris 8 (char tmp[64];): buffer sementara berukuran 64 byte. Karena tipe elemen tidak diketahui, kita tidak bisa menulis int tmp;. Buffer byte menjadi solusi universal.
  3. Baris 9-12 (memcpy(tmp, a, sz); memcpy(a, b, sz); memcpy(b, tmp, sz);): tiga langkah pertukaran. memcpy menyalin sz byte dari sumber ke tujuan tanpa peduli tipe data.
  4. Baris 17 (swap(&x, &y, sizeof(int));): pemanggil menyuplai ukuran elemen lewat sizeof(int). Tanpa ukuran ini, swap tidak tahu apakah harus menukar 4 byte atau 8.
  5. Baris 22 (swap(&d1, &d2, sizeof(double));): fungsi yang sama dipakai untuk double—tidak ada perubahan kode sama sekali. Inilah esensi generic programming.

Pola print_array Generik dengan Callback #

Saat kita menulis fungsi generik, sering kali diperlukan callback untuk operasi yang bergantung tipe: formatting, perbandingan, atau destruktor. Contoh berikut menggabungkan void * dengan callback untuk mencetak array tipe apa pun.

c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

/* Print elemen generik memakai callback format */
typedef void (*PrintFn)(const void *);

void print_array(const void *arr, size_t n, size_t sz, PrintFn pf) {
    const char *p = (const char *)arr;
    printf("[");
    for (size_t i = 0; i < n; i++) {
        if (i) printf(", ");
        pf(p + i * sz);
    }
    printf("]\n");
}

static void print_int(const void *v) {
    printf("%d", *(const int *)v);
}
static void print_double(const void *v) {
    printf("%.2f", *(const double *)v);
}
static void print_str(const void *v) {
    printf("\"%s\"", *(const char *const *)v);
}

/* Comparator generik untuk int */
static int cmp_int(const void *a, const void *b) {
    int ia = *(const int *)a;
    int ib = *(const int *)b;
    return (ia > ib) - (ia < ib);
}

int main(void) {
    int arr_i[] = {42, 17, 9, 31, 24};
    double arr_d[] = {3.14, 1.41, 2.71, 0.58};
    const char *arr_s[] = {"mangga", "apel", "jeruk"};

    print_array(arr_i, 5, sizeof(int), print_int);
    print_array(arr_d, 4, sizeof(double), print_double);
    print_array(arr_s, 3, sizeof(const char *), print_str);

    /* sortir array int dengan qsort generik */
    qsort(arr_i, 5, sizeof(int), cmp_int);
    printf("setelah sort: ");
    print_array(arr_i, 5, sizeof(int), print_int);

    return 0;
}

Output yang Dihasilkan:

text
[42, 17, 9, 31, 24]
[3.14, 1.41, 2.71, 0.58]
["mangga", "apel", "jeruk"]
setelah sort: [9, 17, 24, 31, 42]

Bagian kunci adalah const char *p = (const char *)arr; di dalam print_array. Karena void * tidak mendukung aritmatika pointer langsung, kita cast ke char * (byte pointer) terlebih dahulu. Lalu p + i * sz adalah alamat elemen ke-i—sama persis dengan arr[i] jika tipe elemen diketahui. Callback pf menerima const void * dan tahu tipe aslinya, sehingga bisa melakukan cast balik ke const int * dan mencetak.

Batasan void * #

void * memberikan fleksibilitas, tetapi dengan biaya:

  • Tidak ada type safety: compiler tidak bisa mencegah kita mengirim int * ke fungsi yang sebenarnya memerlukan double *. Bug ini hanya muncul saat runtime, biasanya sebagai data sampah.
  • Tidak bisa di-dereferensi langsung: *p di mana p adalah void * tidak valid. Harus cast dulu ke tipe konkret.
  • Aritmatika pointer terbatas: p + 1 di mana p bertipe void * bukan ekstensi standar—GCC mengizinkannya sebagai ekstensi (menganggap void berukuran 1 byte), tetapi kode tidak portabel ke compiler lain.
  • Callback boilerplate: untuk setiap tipe elemen, kita harus menulis callback terpisah—memori ekstra yang tidak diperlukan di C++ template.