Topik ini membahas preprocessor, yaitu tahap pengolahan teks yang berjalan sebelum compiler benar-benar menerjemahkan kode C++. Pembahasan mencakup direktif #include, include guard, dan kompilasi bersyarat, lalu ditutup dengan perbandingan makro terhadap constexpr. Setelah membaca topik ini, pembaca memahami apa yang terjadi pada berkas sumber sebelum dikompilasi serta alasan kode C++ modern lebih memilih constexpr dan fungsi biasa daripada makro.
Tugas preprocessor dalam pipeline#
Preprocessor adalah tahap pertama pipeline kompilasi yang bekerja pada level teks, bukan pada tata bahasa C++. Preprocessor membaca berkas sumber baris demi baris dan mengeksekusi direktif yang diawali tanda #, misalnya #include, #define, dan #ifdef. Hasil tahap ini adalah unit translasi (translation unit) yang sudah bersih dari direktif, kemudian diteruskan ke compiler untuk diperiksa sintaks dan tipenya.
Karena bekerja murni pada teks, preprocessor tidak memahami tipe data, lingkup, maupun kelas. Ia hanya menempelkan, menggantikan, dan membuang potongan teks sesuai perintahnya. Sifat inilah yang membuat makro berguna sekaligus berisiko: satu #define dapat mengubah banyak baris kode tanpa ada pemeriksaan tipe sama sekali. Perilaku tersebut paling mudah diamati lewat percobaan berikut dengan berkas yang menyertakan header lokal.
Include guard dan hasil pra-pemrosesan#
Masalah paling umum pada pemakaian #include adalah penyisipan ganda. Apabila dua header saling menyertakan, isi sebuah header dapat disalin dua kali ke dalam satu unit translasi, sehingga prototipe yang sama muncul dua kali dan kompilasi gagal. Solusinya adalah include guard, yaitu pola #ifndef, #define, dan #endif yang mengapit seluruh isi header.
Berkas pertama memuat include guard dan sebuah prototipe fungsi. Nama jaga SAPAAN_H dipilih unik agar tidak bertabrakan dengan header lain di proyek yang sama.
// tidak-dijalankan (header; dikompilasi bersama utama.cpp melalui blok bash di bawah)
#ifndef SAPAAN_H
#define SAPAAN_H
void sapa(const char* nama);
#endifDua unit translasi berikut memakai header itu. Berkas utama.cpp bahkan menyertakan sapaan.h dua kali untuk membuktikan bahwa include guard menahan penyisipan kedua.
// tidak-dijalankan (menyertakan header lokal; dibangun lewat blok bash di bawah)
#include "sapaan.h"
#include "sapaan.h"
int main() {
sapa("dunia");
return 0;
}// tidak-dijalankan (menyertakan header lokal; dibangun lewat blok bash di bawah)
#include <iostream>
#include "sapaan.h"
void sapa(const char* nama) {
std::cout << "Halo, " << nama << "!\n";
}Opsi -E menyuruh compiler menjalankan preprocessor saja, lalu mencetak hasilnya tanpa melanjutkan ke kompilasi. Jalankan perintah berikut pada direktori yang memuat ketiga berkas di atas.
# pra-proses utama.cpp tanpa melanjutkan ke kompilasi
g++ -E utama.cpp# 0 "utama.cpp"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "utama.cpp"
# 1 "sapaan.h" 1
void sapa(const char* nama);
# 3 "utama.cpp" 2
int main() {
sapa("dunia");
return 0;
}Keluaran di atas memperlihatkan dua hal. Pertama, prototipe void sapa hanya muncul satu kali meskipun sapaan.h disertakan dua kali, karena pada penyisipan kedua makro SAPAAN_H sudah terdefinisi sehingga blok #ifndef dilewati seluruhnya. Kedua, baris semacam # 1 "sapaan.h" 1 adalah penanda baris (linemarker) yang dibuat preprocessor agar compiler mengetahui asal setiap potongan kode. Penanda ini bukan kode C++ dan tidak ikut dikompilasi. Kompilasi utuhnya dilakukan pada kedua unit translasi sekaligus.
# kompilasi kedua unit translasi sekaligus, lalu jalankan programnya
g++ utama.cpp sapa.cpp -o /tmp/sapa-halo && /tmp/sapa-haloHalo, dunia!Kompilasi bersyarat#
Kompilasi bersyarat (conditional compilation) memilih cabang kode yang ikut dikompilasi berdasarkan keberadaan atau nilai sebuah makro. Direktif #ifdef menguji apakah makro terdefinisi, #else memberi cabang cadangan, dan #endif menutup bloknya. Pola ini dipakai untuk saklar fitur, kode khusus platform, dan debug.
#include <iostream>
#define MODE_ANGKA
int main() {
#ifdef MODE_ANGKA
std::cout << "Mode angka: item dihitung dengan bilangan bulat\n";
#else
std::cout << "Mode teks: item ditampilkan sebagai string\n";
#endif
std::cout << "Nilai __cplusplus: " << __cplusplus << '\n';
return 0;
}Mode angka: item dihitung dengan bilangan bulat
Nilai __cplusplus: 201703Makro __cplusplus didefinisikan otomatis oleh compiler dan berisi tahun standar yang aktif, yaitu 201703L untuk C++17. Selain didefinisikan di dalam berkas, makro juga dapat diatur dari command line melalui opsi -D, misalnya g++ -DDEBUG main.cpp. Dengan cara itu satu berkas sumber dapat dibangun dalam beberapa variasi tanpa perlu disunting, kebiasaan yang lazim pada berbagai sistem build.
Makro mirip fungsi dan kekurangannya#
Makro mirip fungsi (function-like macro) menerima argumen, tetapi tetap bekerja dengan substitusi teks, bukan pemanggilan fungsi. Setiap kemunculan parameter diganti oleh teks argumennya, sehingga argumen yang mengandung efek samping dapat dievaluasi lebih dari satu kali. Program berikut membandingkan makro MAKS dengan fungsi maks yang setara.
#include <iostream>
#define MAKS(a, b) ((a) > (b) ? (a) : (b))
inline int maks(int a, int b) {
return a > b ? a : b;
}
int main() {
int i = 3;
std::cout << "MAKS(++i, 2): " << MAKS(++i, 2) << '\n';
std::cout << "i setelah makro: " << i << '\n';
int j = 3;
std::cout << "maks(++j, 2): " << maks(++j, 2) << '\n';
std::cout << "j setelah fungsi: " << j << '\n';
return 0;
}MAKS(++i, 2): 5
i setelah makro: 5
maks(++j, 2): 4
j setelah fungsi: 4Nilai i bertambah dua kali karena substitusi teks menghasilkan ekspresi (++i) > (2) ? (++i) : (2), sehingga ++i dieksekusi pada kondisi dan sekali lagi pada cabang yang dipilih. Fungsi maks mengevaluasi argumennya tepat satu kali, sehingga j hanya bertambah sekali. Tanda kurung pada tiap parameter makro juga wajib. Tanpa tanda kurung, ekspresi seperti MAKS(a + 1, b) akan menghasilkan perhitungan yang salah karena prioritas operator.
Mengapa constexpr lebih disarankan#
Untuk konstanta dan fungsi kecil, C++ modern menyediakan constexpr yang menempati ranah bahasa, bukan ranah teks. Konstanta constexpr memiliki tipe yang diperiksa compiler, mengikuti aturan lingkup dan namespace, serta tetap terbaca pada debugger. Tabel berikut merangkum perbedaannya dengan makro.
| Aspek | Makro | constexpr |
|---|---|---|
| Pemeriksaan tipe | Tidak ada, murni teks | Ada, lengkap |
| Lingkup | Sejak titik definisi sampai akhir berkas | Mengikuti aturan lingkup C++ |
| Evaluasi argumen | Bisa berulang, bergantung pemakaian | Tepat satu kali |
| Penamaan | Kolisi global mudah terjadi | Aman di dalam namespace dan kelas |
Makro yang menggantikan fungsi kecil sebaiknya ditulis ulang sebagai fungsi inline, fungsi constexpr, atau template. Preprocessor tetap diperlukan untuk #include dan pengaturan build, tetapi pekerjaan perhitungan dan penamaan konstanta sudah dipindahkan ke bahasa itu sendiri. Pembagian tugas ini membuat kode lebih mudah diperiksa tipe dan lebih mudah dilacak ketika terjadi error.
Ringkasan#
- Preprocessor bekerja pada teks sebelum kompilasi dan mengeksekusi direktif yang diawali
#. - Include guard dengan pola
#ifndef,#define, dan#endifmencegah isi header disisipkan dua kali. #ifdefdan#ifmemilih cabang kode saat kompilasi, dan nilai makro dapat diatur melalui opsi-D.- Makro tidak memiliki pemeriksaan tipe dan dapat mengevaluasi argumen berulang, sehingga
constexprdan fungsi biasa lebih disarankan sebagai penggantinya.
Referensi#
- Preprocessor — cppreference.com — Ringkasan seluruh direktif preprocessor C++ beserta perilakunya.
- The C Preprocessor — GNU Project
— Manual preprocessor GCC, termasuk opsi
-E,-D, dan penanda baris. - constexpr declaration specifier — cppreference.com
— Aturan evaluasi konstanta
constexprsebagai pengganti makro.