Pemrograman compile-time

Fungsi constexpr, consteval, constinit, dan static_assert.

Daftar isi

Topik ini membahas pemrograman compile-time, yaitu perhitungan yang diselesaikan compiler sebelum program dijalankan. Alatnya adalah fungsi constexpr, penanda consteval dan constinit dari C++20, serta pemeriksaan static_assert. Setelah membaca topik ini, pembaca dapat memindahkan perhitungan yang nilainya sudah pasti ke saat kompilasi, sehingga program berjalan lebih ringan dan kesalahan nilai terdeteksi jauh sebelum versi pertama diuji.

Fungsi dan variabel constexpr#

Fungsi constexpr boleh dipanggil pada konteks apa pun. Apabila semua argumennya adalah ekspresi konstanta, hasilnya dihitung pada saat kompilasi, sedangkan pemanggilan dengan nilai runtime dijalankan seperti fungsi biasa. Variabel constexpr mewajibkan penginisialisasinya berupa ekspresi konstanta, sehingga nilai variabel tersebut terpaku sejak kompilasi. Program berikut memakai satu fungsi faktorial untuk kedua mode tersebut.

cpp faktorial.cpp
#include <iostream>

constexpr unsigned long long faktorial(unsigned int n) {
    return n <= 1 ? 1ULL : n * faktorial(n - 1);
}

// Dipanggil dengan konstanta, sehingga dihitung sejak kompilasi.
constexpr unsigned long long dua_puluh = faktorial(20);

static_assert(faktorial(5) == 120, "faktorial(5) harus bernilai 120");

int main() {
    std::cout << "faktorial(5): " << faktorial(5) << '\n';
    std::cout << "faktorial(20): " << dua_puluh << '\n';

    unsigned int n = 10;
    std::cout << "faktorial(" << n << ") saat program berjalan: " << faktorial(n) << '\n';
    return 0;
}
text Keluaran
faktorial(5): 120
faktorial(20): 2432902008176640000
faktorial(10) saat program berjalan: 3628800

Baris dua_puluh mengikat hasil faktorial(20) sebagai konstanta kompilasi, sedangkan pemanggilan faktorial(n) di main tetap sah meskipun n berubah pada saat program berjalan. Satu definisi, dua waktu eksekusi. Pola ini menggantikan tabel nilai yang ditulis manual maupun makro yang dulu menjadi satu-satunya pilihan.

Batasan mengenai apa yang boleh ditulis di dalam fungsi constexpr mengikuti standar yang dipakai. Sejak C++14, badannya boleh memuat variabel lokal, perulangan, dan percabangan, sehingga algoritma yang lazim dapat ditulis sebagaimana fungsi biasa. Yang tetap dilarang pada C++17 adalah variabel static lokal, alokasi memori dinamis, serta pemanggilan fungsi yang bukan constexpr, karena semuanya belum dapat dihitung compiler. Apabila salah satu larangan dilanggar, pemanggilan pada konteks konstanta menjadi error, sedangkan pemanggilan runtime tetap diperbolehkan.

Dua sifat yang menyertai constexpr juga perlu dicatat. Fungsi constexpr bersifat inline, sehingga definisinya boleh ditempatkan di header dan dipakai lintas berkas tanpa melanggar aturan one definition rule. Variabel constexpr pada tingkat namespace juga implisit const dan berlaku sebagai ekspresi konstanta untuk konteks lain, misalnya sebagai batas array atau parameter template.

static_assert sebagai penjaga invarian#

static_assert menguji ekspresi konstanta pada saat kompilasi dan menghentikan build apabila nilainya false. Pada program di atas, baris static_assert(faktorial(5) == 120) memastikan definisi rekursifnya benar, dan perubahan yang merusak perhitungan langsung gagal kompilasi dengan pesan yang ditulis sendiri. Sejak C++17, pesan bersifat opsional, sehingga static_assert(faktorial(5) == 120) juga sah.

Pemakaian yang lazim adalah menjaga hubungan antar nilai, misalnya memastikan kapasitas buffer mencukupi jumlah kolom, ukuran tabel konstanta sesuai rencana, atau konversi antar satuan yang tidak boleh melenceng. Invarian semacam ini tidak mungkin diuji dengan assert biasa tanpa menunggu program berjalan, dan sering kali tidak tercakup oleh uji otomatis sekalipun. Pemeriksaan pada kompilasi memberi jaminan yang berlaku untuk setiap build.

Perulangan dan tabel konstanta#

Perulangan di dalam fungsi constexpr membuka cara membangun tabel nilai yang dihitung sekaligus pada saat kompilasi. Array constexpr yang diinisialisasi dari hasil fungsi constexpr menjadi data yang dipasang pada biner program, tanpa perhitungan apa pun ketika program berjalan. Tabel semacam ini menggantikan pola lama menulis konstanta satu per satu maupun mengisi tabel pada awal main. Pemeriksaan static_assert pada elemennya menjaga agar hasil perhitungannya sesuai rencana.

cpp pangkat.cpp
#include <iostream>

constexpr unsigned long long pangkat(unsigned int dasar, unsigned int n) {
    unsigned long long hasil = 1;
    for (unsigned int i = 0; i < n; ++i) {
        hasil *= dasar;
    }
    return hasil;
}

int main() {
    // Tabel dibangun compiler saat kompilasi, bukan saat program berjalan.
    constexpr unsigned long long tabel[] = {
        pangkat(2, 1),
        pangkat(2, 4),
        pangkat(2, 8),
        pangkat(2, 16),
    };

    static_assert(tabel[2] == 256);

    for (unsigned long long nilai : tabel) {
        std::cout << nilai << '\n';
    }
    return 0;
}
text Keluaran
2
16
256
65536

Fungsi pangkat menggunakan variabel lokal dan perulangan for, bentuk yang sah untuk fungsi constexpr sejak C++14. Empat pemanggilannya di dalam inisialisasi tabel dievaluasi compiler, lalu hasilnya ditempatkan sebagai data konstanta, dan static_assert(tabel[2] == 256) membuktikan nilainya sudah tersedia sebelum program berjalan. Apabila salah satu argumennya berganti menjadi nilai runtime, inisialisasi array tersebut langsung menjadi error kompilasi, karena array constexpr menuntut penginisialisasi berupa ekspresi konstanta. Perhatikan bahwa faktorial pada program pertama ditulis rekursif, sedangkan pangkat di sini memilih perulangan, dan keduanya sah. Bentuk yang dipilih boleh mengikuti keterbacaan, karena compiler menghitung keduanya dengan cara yang sama.

consteval dan constinit (C++20)#

constexpr tidak menuntut evaluasi saat kompilasi, sehingga pemanggilan yang sebenarnya bisa dihitung di awal dapat lolos sebagai pemanggilan runtime tanpa ada peringatan. C++20 menambahkan dua penanda yang memperketat hal ini. Fungsi consteval wajib menghasilkan konstanta kompilasi, dan setiap pemanggilan dengan nilai runtime adalah error.

Deklarasi constinit menjamin variabel global diinisialisasi pada saat kompilasi, yaitu static initialization. Jaminan itu menutup risiko urutan inisialisasi dinamis antar unit translasi yang menyebabkan variabel dibaca sebelum terisi. Perlu diingat, constinit tidak membuat variabel menjadi const, sehingga nilainya tetap boleh berubah saat program berjalan.

Kedua penanda memerlukan standar C++20 ke atas. Sandbox jalur ini membangun berkas tunggal dengan standar C++17 secara default, sehingga contoh berikut dibangun secara eksplisit dengan opsi -std=c++23.

cpp waktu-kompilasi.cpp
// tidak-dijalankan (butuh opsi -std=c++23; perintah build ada di blok bash di bawah)
#include <iostream>

// consteval: fungsi yang wajib menghasilkan konstanta saat kompilasi.
consteval int kuadrat(int n) {
    return n * n;
}

// constinit: variabel global dijamin diinisialisasi saat kompilasi.
constinit int ambang = kuadrat(7);

int main() {
    static_assert(kuadrat(5) == 25);

    std::cout << "kuadrat(9): " << kuadrat(9) << '\n';
    std::cout << "ambang: " << ambang << '\n';
    return 0;
}
bash
# bangun dengan standar C++23 lalu jalankan programnya
g++ -std=c++23 waktu-kompilasi.cpp -o /tmp/waktu-kompilasi && /tmp/waktu-kompilasi
text Keluaran
kuadrat(9): 81
ambang: 49

Nilai ambang dihitung compiler dan dipasang pada bagian data program sebelum main dimulai. Pemanggilan kuadrat(9) di dalam main tetap diterima karena argumennya konstanta, dan hasilnya kembali sebagai konstanta juga.

Kegagalan kompilasi sebagai umpan balik#

Sifat ketat consteval terlihat jelas ketika fungsinya dipanggil dengan nilai yang bukan konstanta. Berkas berikut sengaja salah agar pesan errornya dapat dibaca.

cpp gagal.cpp
// tidak-dijalankan (program ini sengaja gagal dikompilasi; diperiksa lewat blok bash)
#include <iostream>

consteval int kuadrat(int n) {
    return n * n;
}

int main() {
    int x = 4;
    int y = kuadrat(x);   // x bukan konstanta saat kompilasi
    std::cout << y << '\n';
    return 0;
}
bash
# coba kompilasi berkas yang sengaja salah, tanpa menautkan program
g++ -std=c++23 -c gagal.cpp -o /tmp/gagal.o
text Keluaran
gagal.cpp: In function ‘int main()’:
gagal.cpp:10:20: error: call to consteval function ‘kuadrat(x)’ is not a constant expression
   10 |     int y = kuadrat(x);   // x bukan konstanta saat kompilasi
      |             ~~~~~~~^~~
gagal.cpp:10:20: error: the value of ‘x’ is not usable in a constant expression
gagal.cpp:9:9: note: ‘int x’ is not const
    9 |     int x = 4;
      |         ^

Compiler menolak pemanggilan itu karena x bukan ekspresi konstanta, dan pesannya menunjuk baris serta alasannya. Perbaikannya adalah mendeklarasikan x sebagai constexpr atau memberikan konstanta langsung. Mekanisme umpan balik seperti inilah yang menjadikan pemrograman compile-time aman dipakai: kesalahan penggunaan tidak pernah menyelinap ke program akhir.

Ringkasan#

  • Fungsi constexpr dihitung saat kompilasi apabila argumennya konstanta, dan tetap dapat dipanggil sebagai fungsi biasa.
  • static_assert menguji invarian antar konstanta pada setiap build dan menghentikan kompilasi bila dilanggar.
  • consteval mewajibkan hasil konstanta kompilasi, sedangkan constinit menjamin inisialisasi statis tanpa membuat variabel konstan.
  • Keduanya memerlukan standar C++20 ke atas, misalnya melalui opsi -std=c++23, dan penyalahgunaannya dilaporkan sebagai error kompilasi.
  • Perulangan dan array constexpr membangun tabel nilai yang dipasang compiler pada biner program sebelum main dimulai.

Referensi#