Preprocessor dan makro

Include guard, kompilasi bersyarat, dan alasan memilih constexpr.

Daftar isi

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.

cpp sapaan.h
// tidak-dijalankan (header; dikompilasi bersama utama.cpp melalui blok bash di bawah)
#ifndef SAPAAN_H
#define SAPAAN_H

void sapa(const char* nama);

#endif

Dua unit translasi berikut memakai header itu. Berkas utama.cpp bahkan menyertakan sapaan.h dua kali untuk membuktikan bahwa include guard menahan penyisipan kedua.

cpp utama.cpp
// tidak-dijalankan (menyertakan header lokal; dibangun lewat blok bash di bawah)
#include "sapaan.h"
#include "sapaan.h"

int main() {
    sapa("dunia");
    return 0;
}
cpp sapa.cpp
// 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.

bash
# pra-proses utama.cpp tanpa melanjutkan ke kompilasi
g++ -E utama.cpp
text Keluaran
# 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.

bash
# kompilasi kedua unit translasi sekaligus, lalu jalankan programnya
g++ utama.cpp sapa.cpp -o /tmp/sapa-halo && /tmp/sapa-halo
text Keluaran
Halo, 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.

cpp mode.cpp
#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;
}
text Keluaran
Mode angka: item dihitung dengan bilangan bulat
Nilai __cplusplus: 201703

Makro __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.

cpp pitfall.cpp
#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;
}
text Keluaran
MAKS(++i, 2): 5
i setelah makro: 5
maks(++j, 2): 4
j setelah fungsi: 4

Nilai 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.

AspekMakroconstexpr
Pemeriksaan tipeTidak ada, murni teksAda, lengkap
LingkupSejak titik definisi sampai akhir berkasMengikuti aturan lingkup C++
Evaluasi argumenBisa berulang, bergantung pemakaianTepat satu kali
PenamaanKolisi global mudah terjadiAman 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 #endif mencegah isi header disisipkan dua kali.
  • #ifdef dan #if memilih cabang kode saat kompilasi, dan nilai makro dapat diatur melalui opsi -D.
  • Makro tidak memiliki pemeriksaan tipe dan dapat mengevaluasi argumen berulang, sehingga constexpr dan fungsi biasa lebih disarankan sebagai penggantinya.

Referensi#