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.
#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;
}Versi program: 2
Program selesaiKondisi 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.
# mengompilasi varian dengan makro MODE_RINCI terdefinisi
gcc -DMODE_RINCI diagnostik.c -o diagnostik-rinci
./diagnostik-rinciVersi program: 2
Mode rinci aktif, setiap langkah dilaporkan
Program selesaiDua 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.
// tidak-dijalankan
#ifndef UKURAN_BUFFER
#define UKURAN_BUFFER 1024
#endifApabila 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.
// tidak-dijalankan
struct Titik {
int x;
int y;
};// 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;
}# mengompilasi berkas yang memuat header yang sama dua kali
gcc uji-rusak.c -o uji-rusakIn 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.
// tidak-dijalankan
#ifndef TITIK_H
#define TITIK_H
struct Titik {
int x;
int y;
};
#endif// 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;
}# mengompilasi versi yang header-nya sudah ber-guard
gcc uji-aman.c -o uji-aman
./uji-amanTitik (3, 4)Diagram berikut merangkum keputusan yang diambil preprosesor setiap kali header ber-guard dimuat.
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.
// tidak-dijalankan
#pragma once
struct Titik {
int x;
int y;
};Perbandingan kedua bentuknya disajikan pada tabel berikut.
| Aspek | Include guard | #pragma once |
|---|---|---|
| Jaminan standar C | dijamin oleh standar | bukan bagian dari standar |
| Bentuk | tiga arahan preproses | satu baris di awal header |
| Sumber kesalahan | nama makro harus unik | tidak ada nama yang bisa salah |
| Dukungan kompiler | semua kompiler C | GCC, 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 melaporkanunterminated #ifdi 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_Huntukgeometri.h. - Mengira
#ifmenilai variabel program. Kondisi#ifhanya boleh berupa ekspresi konstanta yang diketahui saat preproses, sehingga variabelintbiasa tidak dapat dipakai. Percabangan berdasarkan nilai saat program berjalan tetap tugasifbiasa. - Meletakkan kode di luar blok
#ifdefyang 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#ifndefmemilih bagian kode pada waktu preproses, dan cabang yang tidak terpenuhi tidak pernah dikompilasi. - Opsi
-Dmendefinisikan makro dari baris perintah, sehingga satu kode sumber dapat menghasilkan beberapa varian build. - Include guard mencegah penyertaan ganda header melalui makro penanda, sedangkan
#pragma oncememberi hasil serupa dalam satu baris tanpa jaminan standar. - Nama makro guard diturunkan dari nama berkasnya dan harus unik agar header tidak saling menimpa.
Referensi#
- Conditionals — GCC, the GNU Preprocessor
— Kaidah arahan bersyarat
#if,#elif,#else, dan#ifdef. - Once-Only Headers — GCC, the GNU Preprocessor — Cara kerja include guard beserta penjelasan penyertaan ganda.
- Conditional inclusion — cppreference.com
— Rumusan standar untuk penyertaan bersyarat beserta operator
defined.