Kompilasi Bersyarat

Arahan #ifdef dan #ifndef, include guard, #pragma once, dan varian build.

Daftar isi

Preprosesor tidak hanya menyalin berkas dan mengembangkan makro, tetapi juga dapat memilih bagian kode mana yang layak dikompilasi. Topik ini membahas kompilasi bersyarat (conditional compilation) dengan arahan #if, #ifdef, dan #ifndef, cara mencegah penyertaan ganda header melalui include guard dan #pragma once, serta pemakaian varian build dari satu kode sumber yang sama. Setelah membacanya, satu proyek dapat menghasilkan beberapa varian program tanpa menduplikasi kode sumbernya.

Arahan #if, #ifdef, dan #endif#

Arahan bersyarat menyerupai percabangan if, tetapi kondisinya dinilai oleh preprosesor sebelum kompilasi dimulai, dan cabang yang tidak terpenuhi dihapus dari teks program. Kondisinya berupa ekspresi konstanta yang melibatkan makro dan operator perbandingan, sedangkan #ifdef khusus menanyakan keberadaan sebuah makro tanpa mempedulikan nilainya. Program berikut memakai keduanya.

c diagnostik.c
#include <stdio.h>

#define VERSI 2

int main(void) {
#if VERSI >= 2
    printf("Versi program: 2\n");
#elif VERSI == 1
    printf("Versi program: 1\n");
#else
    printf("Versi program tidak diketahui\n");
#endif

#ifdef MODE_RINCI
    printf("Mode rinci aktif, setiap langkah dilaporkan\n");
#endif
    printf("Program selesai\n");
    return 0;
}
text Keluaran
Versi program: 2
Program selesai

Kondisi VERSI >= 2 terpenuhi, sedangkan blok #ifdef MODE_RINCI dihapus karena makro MODE_RINCI tidak pernah didefinisikan dalam kode. Makro tersebut dapat didefinisikan dari baris perintah kompilasi melalui opsi -D, dan cara inilah varian program dihasilkan tanpa menyentuh kode sumbernya.

bash
# mengompilasi varian dengan makro MODE_RINCI terdefinisi
gcc -DMODE_RINCI diagnostik.c -o diagnostik-rinci
./diagnostik-rinci
text Keluaran
Versi program: 2
Mode rinci aktif, setiap langkah dilaporkan
Program selesai

Dua perintah kompilasi pada topik ini menghasilkan dua varian dari berkas yang sama. Pola tersebut adalah bentuk paling sederhana dari varian build (build variant), yaitu satu kode sumber dengan sejumlah variasi perilaku yang dipilih pada waktu kompilasi. Skrip build atau alat seperti make biasanya menyimpan daftar opsi -D milik tiap varian, misalnya varian uji yang menyalakan pelacakan dan varian rilis yang mematikannya.

Nilai bawaan yang dapat ditimpa#

Pola #ifndef menyediakan nilai bawaan bagi sebuah makro sekaligus memberi kesempatan pemanggil build untuk menimpanya. Definisi hanya dilakukan apabila makronya belum ada, sehingga opsi -D dari baris perintah menang atas nilai bawaan di dalam kode.

c Contoh: nilai bawaan dengan #ifndef
// tidak-dijalankan
#ifndef UKURAN_BUFFER
#define UKURAN_BUFFER 1024
#endif

Apabila program dikompilasi tanpa opsi tambahan, UKURAN_BUFFER bernilai 1024. Kompilasi dengan gcc -DUKURAN_BUFFER=4096 menjadikan makro itu bernilai 4096 karena #ifndef menemukan makronya sudah ada dan melompati definisi bawaan. Pola ini dipakai luas pada pustaka untuk mengatur kapasitas, batas waktu, dan pemilihan fitur, karena kode sumbernya tetap satu tanpa perlu berkas varian terpisah.

Idiom ini menghubungkan nilai yang ditulis di kode dengan kebijakan yang ditentukan pemanggil build. Nilai bawaan memberi program perilaku yang wajar tanpa konfigurasi apa pun, sedangkan penimpaan lewat -D menampung kebutuhan lingkungan yang berbeda tanpa menyentuh kode sumber. Apabila nilainya bertambah banyak, daftar penimpaan itu biasanya dikumpulkan pada satu berkas yang dibaca skrip build agar seluruh varian konsisten. Pola yang sama berlaku untuk konstanta selain bilangan, misalnya nama berkas bawaan atau pilihan format keluaran.

Include guard#

Header dapat termuat lebih dari satu kali tanpa disadari, misalnya satu berkas memuat dua header yang masing-masing memuat header yang sama. Deklarasi fungsi memang boleh diulang, tetapi definisi tipe seperti struct tidak, sehingga penyertaan ganda berakhir sebagai error. Berkas berikut sengaja ditulis tanpa pengaman untuk memperlihatkan kegagalannya.

c titik-rusak.h
// tidak-dijalankan
struct Titik {
    int x;
    int y;
};
c uji-rusak.c
// tidak-dijalankan
#include <stdio.h>
#include "titik-rusak.h"
#include "titik-rusak.h"

int main(void) {
    struct Titik p = {3, 4};
    printf("Titik (%d, %d)\n", p.x, p.y);
    return 0;
}
bash
# mengompilasi berkas yang memuat header yang sama dua kali
gcc uji-rusak.c -o uji-rusak
text Keluaran
In file included from uji-rusak.c:4:
titik-rusak.h:2:8: error: redefinition of ‘struct Titik’
    2 | struct Titik {
      |        ^~~~~
In file included from uji-rusak.c:3:
titik-rusak.h:2:8: note: originally defined here
    2 | struct Titik {
      |        ^~~~~

Pesan error itu muncul karena isi header tersalin dua kali ke dalam satu berkas hasil preproses, sehingga struct Titik didefinisikan dua kali. Perbaikannya adalah include guard, yaitu pembungkus tiga arahan yang menahan penyertaan kedua dan seterusnya.

c titik.h
// tidak-dijalankan
#ifndef TITIK_H
#define TITIK_H

struct Titik {
    int x;
    int y;
};

#endif
c uji-aman.c
// tidak-dijalankan
#include <stdio.h>
#include "titik.h"
#include "titik.h"

int main(void) {
    struct Titik p = {3, 4};
    printf("Titik (%d, %d)\n", p.x, p.y);
    return 0;
}
bash
# mengompilasi versi yang header-nya sudah ber-guard
gcc uji-aman.c -o uji-aman
./uji-aman
text Keluaran
Titik (3, 4)

Diagram berikut merangkum keputusan yang diambil preprosesor setiap kali header ber-guard dimuat.

#include "titik.h"makro guardsudah terdefinisi?isi header dilewatiguard didefinisikan,isi header tersalinsudahbelum
Alur keputusan include guard pada setiap penyertaan header

Pemuatan pertama menemukan TITIK_H belum terdefinisi, mendefinisikannya, lalu menyalin isi header. Pemuatan berikutnya menemukan makro yang sudah ada, sehingga seluruh isi antara #ifndef dan #endif dilewati. Nama makro guard lazim diambil dari nama berkasnya dengan huruf besar dan garis bawah, sehingga titik.h menjadi TITIK_H. Kebiasaan itu menjaga keunikan nama guard, karena dua guard yang bertabrakan membuat header kedua diam-diam terlewat, dan kekurangan definisi tipe barulah muncul jauh di berkas lain.

#pragma once#

#pragma once adalah alternatif satu baris yang ditempatkan di awal header tanpa perlu makro penanda sama sekali. Kompilator mengingat berkas mana yang sudah termuat dan melewati penyertaan berikutnya, sehingga hasilnya setara dengan include guard.

c Contoh: header dengan #pragma once
// tidak-dijalankan
#pragma once

struct Titik {
    int x;
    int y;
};

Perbandingan kedua bentuknya disajikan pada tabel berikut.

AspekInclude guard#pragma once
Jaminan standar Cdijamin oleh standarbukan bagian dari standar
Bentuktiga arahan preprosessatu baris di awal header
Sumber kesalahannama makro harus uniktidak ada nama yang bisa salah
Dukungan kompilersemua kompiler CGCC, Clang, dan MSVC

Karena tidak memerlukan nama makro, #pragma once menghindari dua jenis kesalahan manusia, yaitu salah mengetik nama guard dan lupa mengubahnya setelah menyalin header lama sebagai templat. Komprominya terletak pada jaminan, karena perilakunya sepenuhnya ditentukan kompiler dan bukan bagian dari standar C mana pun. Dukungan pada kompiler utama sudah matang, tetapi proyek yang mengutamakan portabilitas umumnya tetap memilih include guard. Beberapa proyek menaruh keduanya sekaligus untuk mengambil keunggulan masing-masing, yaitu kecepatan penulisan dari pragma dan jaminan standar dari guard.

Kesalahan umum#

  • Lupa menutup blok bersyarat dengan #endif. Preprosesor melaporkan unterminated #if di akhir berkas, dan pesannya menunjuk arahan pembuka yang kekurangan penutup. Susun blok bersyarat secara rapi dan beri indentasi pada arahan bersarang agar ketidakseimbangannya terlihat.
  • Memakai nama makro guard yang sama di dua header. Header kedua dianggap sudah termuat dan seluruh isinya terlewat, lalu error baru muncul di berkas lain dengan bentuk tipe yang tidak dikenal. Turunkan nama guard dari nama berkasnya, misalnya GEOMETRI_H untuk geometri.h.
  • Mengira #if menilai variabel program. Kondisi #if hanya boleh berupa ekspresi konstanta yang diketahui saat preproses, sehingga variabel int biasa tidak dapat dipakai. Percabangan berdasarkan nilai saat program berjalan tetap tugas if biasa.
  • Meletakkan kode di luar blok #ifdef yang seharusnya melindunginya. Deklarasi atau definisi yang tertinggal di luar blok tetap dikompilasi pada semua varian, termasuk varian yang seharusnya tidak memuat fitur itu. Periksa jangkauan blok bersyarat setiap kali fitur baru ditambahkan, karena kesalahan jangkauan semacam ini tidak menghasilkan pesan apa pun.

Ringkasan#

  • Arahan #if, #ifdef, dan #ifndef memilih bagian kode pada waktu preproses, dan cabang yang tidak terpenuhi tidak pernah dikompilasi.
  • Opsi -D mendefinisikan makro dari baris perintah, sehingga satu kode sumber dapat menghasilkan beberapa varian build.
  • Include guard mencegah penyertaan ganda header melalui makro penanda, sedangkan #pragma once memberi hasil serupa dalam satu baris tanpa jaminan standar.
  • Nama makro guard diturunkan dari nama berkasnya dan harus unik agar header tidak saling menimpa.

Referensi#