Ketika kita menulis library yang ditargetkan untuk banyak compiler (GCC, Clang, MSVC, vendor embedded), kita tidak bisa berasumsi semua fitur tersedia. Standar C menyediakan beberapa mekanisme deteksi fitur: macro __STDC_VERSION__ untuk versi standar, macro __STDC_NO_ATOMICS/__STDC_NO_THREADS untuk fitur opsional C11, operator preprocessor __has_include (sejak C23, didukung GCC sejak 5.1) untuk cek ketersediaan header, dan __has_builtin/__has_attribute (ekstensi GCC/Clang) untuk cek ekstensi compiler. Bab ini menunjukkan pola penulisan conditional feature support yang membuat kode dapat beradaptasi otomatis dengan compiler yang dipakai—memakai fitur modern bila tersedia, fallback ke implementasi alternatif bila tidak. Kita juga akan melihat strategi penulisan header portabilitas yang dapat di-include di semua compiler tanpa menyebabkan error.

Deteksi via __STDC_VERSION__ #

Cara paling portabel untuk mendeteksi versi standar adalah memeriksa macro __STDC_VERSION__. Macro ini didefinisikan sejak C95; di mode C89/C90, macro ini tidak terdefinisi. Program berikut memeriksa ketersediaan fitur C99, C11, C17, dan C23 secara berurutan:

c
// File: kond_1.c - __STDC_VERSION__ dan feature test macro
#include <stdio.h>

int main(void) {
    printf("Deteksi fitur berdasarkan __STDC_VERSION__:\n");

#if defined(__STDC_VERSION__)
    printf("  __STDC_VERSION__ = %ldL\n", __STDC_VERSION__);
#else
    printf("  __STDC_VERSION__ tidak terdefinisi (mode C89/C90)\n");
#endif

#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
    printf("  // komentar, mixed declaration : tersedia (C99+)\n");
#else
    printf("  // komentar, mixed declaration : tidak tersedia (C89)\n");
#endif

#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
    printf("  _Generic, _Static_assert        : tersedia (C11+)\n");
    #ifdef __STDC_NO_ATOMICS
    printf("  atomics                         : TIDAK didukung\n");
    #else
    printf("  atomics                         : didukung\n");
    #endif
    #ifdef __STDC_NO_THREADS
    printf("  threads.h                       : TIDAK didukung\n");
    #else
    printf("  threads.h                       : didukung\n");
    #endif
#else
    printf("  _Generic, _Static_assert        : tidak tersedia (pre-C11)\n");
#endif

#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201710L
    printf("  ATOMIC_VAR_INIT obsolete        : ya (C17+)\n");
#endif

#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
    printf("  nullptr, constexpr, [[nodiscard]]: tersedia (C23)\n");
#else
    printf("  nullptr, constexpr, [[nodiscard]]: belum final C23\n");
#endif

    return 0;
}

Compile dengan beberapa flag dan amati perbedaannya:

bash
gcc -std=c11 -Wall -Wextra -pedantic -o kond_1_c11 kond_1.c && ./kond_1_c11
gcc -std=c17 -Wall -Wextra -pedantic -o kond_1_c17 kond_1.c && ./kond_1_c17
gcc -std=c2x -Wall -Wextra -pedantic -o kond_1_c2x kond_1.c && ./kond_1_c2x

Output yang Dihasilkan (gcc -std=c11):

text
Deteksi fitur berdasarkan __STDC_VERSION__:
  __STDC_VERSION__ = 201112L
  // komentar, mixed declaration : tersedia (C99+)
  _Generic, _Static_assert        : tersedia (C11+)
  atomics                         : didukung
  threads.h                       : didukung
  nullptr, constexpr, [[nodiscard]]: belum final C23

Output yang Dihasilkan (gcc -std=c17):

text
Deteksi fitur berdasarkan __STDC_VERSION__:
  __STDC_VERSION__ = 201710L
  // komentar, mixed declaration : tersedia (C99+)
  _Generic, _Static_assert        : tersedia (C11+)
  atomics                         : didukung
  threads.h                       : didukung
  ATOMIC_VAR_INIT obsolete        : ya (C17+)
  nullptr, constexpr, [[nodiscard]]: belum final C23

Mari kita telaah baris-baris kode di atas:

  1. Baris 7-11 (#if defined(__STDC_VERSION__) ... #endif): Cek apakah macro terdefinisi sebelum mengakses nilainya. Di mode C89, __STDC_VERSION__ tidak ada; mengakses macro yang tidak terdefinisi di #if akan dianggap 0, tetapi lebih aman pakai defined() eksplisit.
  2. Baris 13-17 (#if ... __STDC_VERSION__ >= 199901L): Cek ketersediaan C99. Angka 199901L adalah kode YYYYMML untuk C99 (Januari 1999). Bila compiler lebih baru, cabang ini aktif dan kita tahu // komentar serta mixed declaration dapat dipakai.
  3. Baris 19-32 (#if ... __STDC_VERSION__ >= 201112L): Cek ketersediaan C11. Di dalam cabang ini, kita juga cek __STDC_NO_ATOMICS dan __STDC_NO_THREADS—dua macro yang didefinisikan compiler jika fitur tersebut tidak didukung. Pada GCC modern, keduanya tidak didefinisikan, yang berarti fitur tersedia.
  4. Baris 34-36 (#if ... __STDC_VERSION__ >= 201710L): Cek C17. Hanya menampilkan informasi tentang ATOMIC_VAR_INIT yang obsolete di C17.
  5. Baris 38-42 (#if ... __STDC_VERSION__ >= 202311L): Cek C23 final. GCC 14 masih memberi 202000L (placeholder), sehingga cabang ini tidak aktif—program akan melaporkan “belum final C23” meskipun kompilernya mendukung sebagian besar fitur C23.

__has_include dan __has_builtin #

Preprocessor C23 menambahkan operator __has_include("header") yang mengembalikan 1 bila header tersedia, 0 bila tidak. Sebelum C23, GCC dan Clang sudah mendukung ini sebagai ekstensi. Pola ini memungkinkan kode memilih header alternatif tanpa menyebabkan compile error. GCC dan Clang juga menyediakan __has_builtin(name) dan __has_attribute(name) untuk mengecek ekstensi compiler:

c
// File: kond_2.c - __has_include dan __has_builtin
#include <stdio.h>

// __has_include: cek apakah header tersedia tanpa menyebabkan error
#if defined(__has_include)
#  if __has_include(<stdatomic.h>)
#    include <stdatomic.h>
#    define PUNYA_ATOMIC 1
#  else
#    define PUNYA_ATOMIC 0
#  endif
#  if __has_include(<threads.h>)
#    include <threads.h>
#    define PUNYA_THREADS 1
#  else
#    define PUNYA_THREADS 0
#  endif
#  if __has_include(<sys/epoll.h>)
#    define PUNYA_EPOLL 1
#  else
#    define PUNYA_EPOLL 0
#  endif
#else
#  define PUNYA_ATOMIC 0
#  define PUNYA_THREADS 0
#  define PUNYA_EPOLL 0
#endif

// __has_builtin: cek apakah GCC builtin tersedia
#if defined(__has_builtin)
#  if __has_builtin(__builtin_expect)
#    define PUNYA_BUILTIN_EXPECT 1
#  else
#    define PUNYA_BUILTIN_EXPECT 0
#  endif
#  if __has_builtin(__builtin_popcount)
#    define PUNYA_BUILTIN_POPCOUNT 1
#  else
#    define PUNYA_BUILTIN_POPCOUNT 0
#  endif
#else
#  define PUNYA_BUILTIN_EXPECT 0
#  define PUNYA_BUILTIN_POPCOUNT 0
#endif

// __has_attribute: cek apakah GCC attribute tersedia
#if defined(__has_attribute)
#  if __has_attribute(packed)
#    define PUNYA_ATTR_PACKED 1
#  else
#    define PUNYA_ATTR_PACKED 0
#  endif
#else
#  define PUNYA_ATTR_PACKED 0
#endif

int main(void) {
    printf("Hasil deteksi fitur:\n");
    printf("  PUNYA_ATOMIC         : %d\n", PUNYA_ATOMIC);
    printf("  PUNYA_THREADS        : %d\n", PUNYA_THREADS);
    printf("  PUNYA_EPOLL          : %d\n", PUNYA_EPOLL);
    printf("  PUNYA_BUILTIN_EXPECT : %d\n", PUNYA_BUILTIN_EXPECT);
    printf("  PUNYA_BUILTIN_POPCOUNT: %d\n", PUNYA_BUILTIN_POPCOUNT);
    printf("  PUNYA_ATTR_PACKED    : %d\n", PUNYA_ATTR_PACKED);

#if PUNYA_BUILTIN_POPCOUNT
    printf("\nContoh __builtin_popcount(0xAB) = %d (jumlah bit 1)\n",
        __builtin_popcount(0xAB));
#endif
    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -pedantic -o kond_2 kond_2.c
./kond_2

Output yang Dihasilkan:

text
Hasil deteksi fitur:
  PUNYA_ATOMIC         : 1
  PUNYA_THREADS        : 1
  PUNYA_EPOLL          : 1
  PUNYA_BUILTIN_EXPECT : 1
  PUNYA_BUILTIN_POPCOUNT: 1
  PUNYA_ATTR_PACKED    : 1

Contoh __builtin_popcount(0xAB) = 5 (jumlah bit 1)

Pada contoh di atas, __has_include(<sys/epoll.h>) mengembalikan 1 di Linux karena header epoll tersedia, tetapi akan mengembalikan 0 di macOS atau Windows. Dengan pola ini, kode kita dapat memilih implementasi berbasis epoll di Linux dan fallback ke select/kqueue di platform lain. __has_builtin(__builtin_popcount) mengembalikan 1 di GCC/Clang; di compiler yang tidak mendukung, kita dapat fallback ke implementasi popcount manual dengan loop.

Pola Header Portabilitas #

Strategi praktis untuk library lintas compiler adalah membuat header portabilitas (mis. portab.h) yang mendeteksi fitur sekali dan mengekspor macro sederhana. Header lain lalu memakai macro tersebut tanpa perlu pemeriksaan #ifdef berulang. Berikut sketsa header portabilitas:

c
// File: portab.h - Header portabilitas lintas compiler
#ifndef PORTAB_H
#define PORTAB_H

/* Versi standar yang aktif */
#if defined(__STDC_VERSION__)
#  define PORTAB_STD_VERSION (__STDC_VERSION__)
#else
#  define PORTAB_STD_VERSION 199000L   /* Asumsi C90 */
#endif

/* Pilihan nullptr */
#if PORTAB_STD_VERSION >= 202311L
#  define PORTAB_NULL nullptr
#elif defined(__cplusplus) && __cplusplus >= 201103L
#  define PORTAB_NULL nullptr
#else
#  define PORTAB_NULL ((void*)0)
#endif

/* Pilihan noreturn */
#if PORTAB_STD_VERSION >= 202311L
#  define PORTAB_NORETURN [[noreturn]]
#elif PORTAB_STD_VERSION >= 201112L
#  define PORTAB_NORETURN _Noreturn
#elif defined(__GNUC__)
#  define PORTAB_NORETURN __attribute__((noreturn))
#else
#  define PORTAB_NORETURN
#endif

/* Pilihan deprecated */
#if PORTAB_STD_VERSION >= 202311L
#  define PORTAB_DEPRECATED(msg) [[deprecated(msg)]]
#elif defined(__GNUC__)
#  define PORTAB_DEPRECATED(msg) __attribute__((deprecated(msg)))
#else
#  define PORTAB_DEPRECATED(msg)
#endif

/* Pilihan likely/unlikely */
#if defined(__GNUC__)
#  define PORTAB_LIKELY(x)   __builtin_expect(!!(x), 1)
#  define PORTAB_UNLIKELY(x) __builtin_expect(!!(x), 0)
#else
#  define PORTAB_LIKELY(x)   (x)
#  define PORTAB_UNLIKELY(x) (x)
#endif

#endif /* PORTAB_H */

Header di atas dapat di-include di compiler manapun (GCC, Clang, MSVC, SDCC). Macro PORTAB_NULL otomatis memilih nullptr di C23/C++11 atau ((void*)0) di C89. Macro PORTAB_NORETURN memilih [[noreturn]] di C23, _Noreturn di C11, atau __attribute__((noreturn)) di GCC tua. Header seperti ini biasanya dipakai sebagai dasar untuk library lintas compiler seperti SDL, libuv, atau c-ares.