Sejauh ini, hampir semua perhitungan pada program kita berlangsung saat program berjalan: fungsi dipanggil, CPU menghitung, hasilnya dipakai. Padahal banyak nilai sudah diketahui jauh sebelum itu — konversi satuan, konstanta matematika, isi tabel konfigurasi yang tidak berubah. Kata kunci constexpr memberi compiler izin untuk menghitung sebuah fungsi pada saat kompilasi, sedangkan consteval (C++20) menjadikan perhitungan itu keharusan. Chapter sebelumnya, Type Traits, membahas perhitungan tentang tipe; chapter ini melengkapinya dengan perhitungan nilai dan aturan kapan compiler menghitungnya.

Fungsi constexpr: Satu Fungsi, Dua Jalur #

Fungsi constexpr adalah fungsi biasa yang diberi kualifikasi constexpr: compiler boleh mengevaluasinya pada saat kompilasi, dan fungsi yang sama tetap dapat dieksekusi saat runtime. Syarat utamanya, tipe kembalian dan seluruh parameter harus literal type — tipe yang bisa dibangun pada saat kompilasi, seperti angka, enum, dan class sederhana. Badan fungsinya juga dibatasi: tanpa goto, tanpa label selain case dan default, serta tanpa variabel berdurasi static atau thread hingga C++23 mengizinkan bentuk static constexpr. Sejak C++17, constexpr sekaligus menyiratkan inline, sehingga fungsi ini lazim didefinisikan lengkap di dalam header.

Program berikut memakai satu fungsi faktorial untuk kedua dunia. Bukti evaluasi compile time kita taruh pada static_assert, yang diperiksa compiler sebelum program dibuat — bila perhitungan meleset, kompilasi yang berhenti, bukan output program. Jalur runtime kita uji lewat trik volatile yang dibedah di bawah.

cpp
// constexpr-demo.cpp — fungsi constexpr: satu fungsi, dua jalur eksekusi
// Kompilasi: g++ -std=c++23 -Wall -Wextra constexpr-demo.cpp -o constexpr-demo

#include <iostream>

// Fungsi constexpr: boleh dievaluasi compiler saat kompilasi maupun
// dieksekusi saat runtime, tergantung konteks pemanggilannya
constexpr int faktorial(int n) {
    int hasil = 1;
    for (int i = 2; i <= n; ++i) {
        hasil *= i;
    }
    return hasil;
}

// if consteval (C++23): 1 bila dievaluasi compiler, 0 bila dieksekusi runtime
constexpr int jalurEksekusi() {
    if consteval {
        return 1;
    } else {
        return 0;
    }
}

// static_assert: bukti evaluasi compile time. Kompilasi tidak akan selesai
// bila hasilnya bukan konstanta yang benar
static_assert(faktorial(5) == 120, "5! harus 120 pada saat kompilasi");
static_assert(faktorial(10) == 3628800, "10! harus 3628800 pada saat kompilasi");
static_assert(jalurEksekusi() == 1, "di dalam static_assert kita berada di jalur kompilasi");

int main() {
    // Konteks yang mewajibkan konstanta: compiler menghitung faktorial(6)
    constexpr int konstEnam = faktorial(6);
    std::cout << "faktorial(6)  = " << konstEnam << " (dihitung saat kompilasi)\n";

    // Argumen konstanta pada konteks biasa: compiler bebas memilih jalurnya
    std::cout << "faktorial(7)  = " << faktorial(7) << " (di konteks biasa)\n";

    // volatile melarang compiler mengganti variabel dengan angkanya,
    // sehingga pemanggilan berikut pasti dieksekusi saat program berjalan
    volatile int masukan = 8;
    std::cout << "faktorial(8)  = " << faktorial(masukan) << " (runtime)\n";

    // Fungsi yang sama kini berjalan di jalur runtime: cabang else yang terpilih
    std::cout << "jalurEksekusi() saat runtime = " << jalurEksekusi() << "\n";

    return 0;
}

Output yang Dihasilkan:

text
faktorial(6)  = 720 (dihitung saat kompilasi)
faktorial(7)  = 5040 (di konteks biasa)
faktorial(8)  = 40320 (runtime)
jalurEksekusi() saat runtime = 0

Bedah baris demi baris:

  1. Baris 8 (constexpr int faktorial(int n)): kualifikasi constexpr menandai fungsi; loop dan variabel lokal di dalamnya sah sejak C++14, sementara standar C++11 menuntut satu return tunggal.
  2. Baris 17-23 (jalurEksekusi): if consteval (C++23) memilih cabang berdasarkan konteks evaluasi; pendahulunya pada C++20 adalah std::is_constant_evaluated() dari <type_traits>.
  3. Baris 27-29 (static_assert(...)): tiga pemeriksaan yang wajib lulus pada saat kompilasi; jalurEksekusi() == 1 di baris 29 membuktikan kita sedang di jalur kompilasi.
  4. Baris 33 (constexpr int konstEnam = faktorial(6)): inisialisasi variabel constexpr menuntut constant expression, sehingga compiler wajib menghitung faktorial(6).
  5. Baris 37 (faktorial(7)): konteks biasa dengan argumen konstanta — standar tidak mewajibkan dan tidak melarang evaluasi compile time; jaminannya hanya hasil yang identik.
  6. Baris 41-42 (volatile int masukan): volatile melarang compiler memperlakukan variabel sebagai konstanta, sehingga pemanggilan pasti berjalan saat runtime — dan jalurEksekusi() tercetak 0.

Kapan Kompilasi, Kapan Runtime #

Aturan pemilihan di balik contoh tadi dapat dirangkum begini: standar mengatakan constexpr menyatakan bahwa evaluasi pada saat kompilasi mungkin, bukan pasti. Compiler hanya diwajibkan menghitung sebuah pemanggilan bila hasilnya dibutuhkan sebagai konstanta — inisialisasi variabel constexpr, kondisi static_assert, ukuran array, atau argumen non-type template parameter. Untuk konteks biasa, compiler bebas memilih jalurnya, dengan jaminan hasilnya identik terhadap fungsi biasa. Sebaliknya, bila ada argumen yang tidak dapat dipakai dalam constant expression — input pengguna, variabel volatile, data dari file — evaluasi compile time tidak mungkin dan fungsi berjalan layaknya fungsi biasa. Tabel berikut merangkum keputusan tersebut.

Konteks pemanggilanContohKapan dievaluasi
Kondisi static_assertstatic_assert(faktorial(5) == 120)wajib saat kompilasi
Inisialisasi variabel constexprconstexpr int c = faktorial(6)wajib saat kompilasi
Argumen NTTP / ukuran arraystd::array<int, faktorial(4)>wajib saat kompilasi
Konteks biasa, argumen konstantastd::cout << faktorial(7)bebas — hasilnya tetap identik
Ada argumen non-konstantafaktorial(masukan)wajib runtime

consteval: Immediate Function #

consteval (C++20) mengunci pilihan tersebut: fungsi yang diberi kualifikasi ini disebut immediate function, dan setiap pemanggilan yang dievaluasi wajib menghasilkan konstanta compile time — bila tidak, kompilasi gagal. Ia pada dasarnya fungsi constexpr dengan syarat lebih ketat: implisit inline, tidak boleh ditulis bersama constexpr, dan tidak dapat diterapkan pada destructor maupun fungsi alokasi. Jangan tertukar: consteval tetap boleh muncul di kode runtime — yang dilarang adalah argumen yang tidak diketahui compiler.

Contoh berikut memakai consteval untuk dua fungsi kecil, salah satunya memanggil fungsi constexpr biasa. Hasilnya kita pakai di tiga konteks berbeda: variabel constexpr, ukuran std::array, dan ekspresi std::cout. Ketiganya sah karena semua argumennya konstanta yang diketahui compiler.

cpp
// consteval-demo.cpp — consteval: fungsi yang wajib menghasilkan konstanta
// Kompilasi: g++ -std=c++23 -Wall -Wextra consteval-demo.cpp -o consteval-demo

#include <array>
#include <iostream>

// consteval (C++20): immediate function — setiap pemanggilan wajib
// menghasilkan konstanta compile time
consteval int kuadrat(int n) {
    return n * n;
}

// Fungsi constexpr biasa boleh dipanggil dari dalam consteval
constexpr int faktorial(int n) {
    int hasil = 1;
    for (int i = 2; i <= n; ++i) {
        hasil *= i;
    }
    return hasil;
}

consteval int perkalianFaktorial(int a, int b) {
    return faktorial(a) * faktorial(b);
}

// Bukti compile time: hasil consteval dipakai dalam static_assert
static_assert(kuadrat(9) == 81, "kuadrat(9) wajib diketahui saat kompilasi");

int main() {
    // Hasil consteval menjadi konstanta yang ditanam compiler
    constexpr int sisi = kuadrat(4);
    std::cout << "kuadrat(4) = " << sisi << "\n";

    // Hasil consteval sah sebagai argumen NTTP (ukuran array)
    std::array<int, kuadrat(3)> matriks{};
    std::cout << "ukuran array = " << matriks.size() << "\n";

    // Pemanggilan pada konteks runtime tetap sah: compiler mengganti
    // pemanggilan dengan hasil konstantanya
    std::cout << "kuadrat(12) = " << kuadrat(12) << "\n";
    std::cout << "perkalianFaktorial(3, 5) = " << perkalianFaktorial(3, 5) << "\n";

    return 0;
}

Output yang Dihasilkan:

text
kuadrat(4) = 16
ukuran array = 9
kuadrat(12) = 144
perkalianFaktorial(3, 5) = 720

Bedah baris demi baris:

  1. Baris 9 (consteval int kuadrat(int n)): deklarasi immediate function; tidak ada kata constexpr di depannya meskipun semantiknya menurun dari fungsi constexpr.
  2. Baris 22-24 (perkalianFaktorial): fungsi consteval boleh memanggil fungsi constexpr; di dalam konteks immediate function, hasil panggilan itu ikut dihitung pada saat kompilasi.
  3. Baris 27 (static_assert): bukti compile time untuk kuadrat(9).
  4. Baris 31 (constexpr int sisi = kuadrat(4)): konteks yang menuntut konstanta cocok dengan janji consteval.
  5. Baris 35 (std::array<int, kuadrat(3)>): hasil consteval sah sebagai argumen NTTP — pola “ukuran di dalam tipe” yang dibedah pada chapter Non-type Template Parameters.
  6. Baris 40-41 (std::cout << kuadrat(12)): pemanggilan pada konteks runtime tetap sah karena 12 dan pasangan 3, 5 adalah konstanta; compiler menyisipkan hasilnya langsung ke kode.

Kesalahan paling umum adalah memberi argumen runtime pada fungsi consteval, dan kesalahan ini terdeteksi sejak kompilasi. Blok berikut memang tidak dapat dikompilasi — dan justru itulah pelajarannya. Pada fungsi constexpr, pemanggilan serupa sah dan hanya berpindah ke jalur runtime.

cpp Pemanggilan consteval dengan argumen runtime (tidak dapat dikompilasi)
// consteval-gagal.cpp — pemanggilan consteval dengan argumen runtime
#include <iostream>

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

int main() {
    int masukan = 7;  // variabel biasa: nilainya baru ada saat program berjalan
    std::cout << kuadrat(masukan) << "\n";
}

Pesan nyata dari g++ 14.2:

text
consteval-gagal.cpp: In function ‘int main()’:
consteval-gagal.cpp:10:25: error: call to consteval function ‘kuadrat(masukan)’ is not a constant expression
   10 |     std::cout << kuadrat(masukan) << "\n";
      |                  ~~~~~~~^~~~~~~~~
consteval-gagal.cpp:10:25: error: the value of ‘masukan’ is not usable in a constant expression
consteval-gagal.cpp:9:9: note: ‘int masukan’ is not const
    9 |     int masukan = 7;  // variabel biasa: nilainya baru ada saat program berjalan
      |         ^~~~~~~

Kompilasi berhenti dengan dua pesan: pesan pertama menyatakan pemanggilan consteval bukan constant expression, pesan kedua menunjuk penyebabnya — masukan bukan konstanta. Tambahkan constexpr pada variabel tersebut dan pemanggilannya langsung sah, karena nilainya kini diketahui compiler. Perhatikan bahwa program tidak pernah dijalankan: kegagalan ini murni fase kompilasi.

std::vector dan Algoritma constexpr #

C++20 membawa evaluasi konstanta ke wilayah baru: std::vector dan std::string menjadi constexpr, dan algoritma pada <algorithm> serta <numeric> dapat dipanggil di dalamnya. Syaratnya satu: alokasi memori harus transient — objek dibuat dan masa hidupnya berakhir dalam satu evaluasi yang sama, sehingga tidak ada memori heap yang terbawa keluar dari saat kompilasi. Dengan aturan itu, kita bisa menyortir dan menghitung rata-rata sebelum program dibuat.

cpp
// constexpr-vector.cpp — std::vector dan algoritma di dalam evaluasi konstanta
// Kompilasi: g++ -std=c++23 -Wall -Wextra constexpr-vector.cpp -o constexpr-vector

#include <algorithm>
#include <iostream>
#include <numeric>
#include <string>
#include <vector>

// std::vector constexpr (C++20): alokasi memori boleh terjadi selama
// evaluasi konstanta, asal wadahnya selesai digunakan dalam evaluasi itu
constexpr int rataRata() {
    std::vector<int> nilai{78, 91, 65, 84, 70};
    std::sort(nilai.begin(), nilai.end());            // algoritma constexpr
    int total = std::accumulate(nilai.begin(), nilai.end(), 0);
    return total / static_cast<int>(nilai.size());
}

// std::string juga constexpr sejak C++20
constexpr int hitungSpasi() {
    std::string kalimat = "kita belajar cpp dengan tekun";
    return std::count(kalimat.begin(), kalimat.end(), ' ');
}

// Bukti compile time: hasil kedua fungsi dipakai sebagai konstanta
static_assert(rataRata() == 77, "rata-rata dari 78+91+65+84+70 dibagi 5");
static_assert(hitungSpasi() == 4, "kalimat berisi empat spasi");

int main() {
    std::cout << "rata-rata nilai = " << rataRata() << "\n";
    std::cout << "jumlah spasi    = " << hitungSpasi() << "\n";
    return 0;
}

Output yang Dihasilkan:

text
rata-rata nilai = 77
jumlah spasi    = 4

Bedah baris demi baris:

  1. Baris 12-17 (rataRata): std::vector lokal dibuat dan masa hidupnya berakhir di dalam satu evaluasi konstanta — persis bentuk alokasi transient yang diizinkan.
  2. Baris 14 (std::sort): algoritma constexpr sejak C++20; pengurutan lima angka terjadi pada fase kompilasi.
  3. Baris 15 (std::accumulate): algoritma dari <numeric> juga constexpr sejak C++20.
  4. Baris 20-23 (hitungSpasi): std::string constexpr sejak C++20; string sementara dibangun lalu selesai di dalam evaluasi.
  5. Baris 26-27 (static_assert): bukti compile time untuk kedua fungsi; pemanggilan ulang di main menghasilkan angka yang sama.

Batas alokasi transient langsung terlihat bila vector ditaruh pada scope namespace: masa hidupnya mencakup seluruh program, sedangkan hasil alokasi pada evaluasi konstanta tidak boleh melewatinya. Variabel global berikut adalah contoh paling mudahnya. Blok ini juga tidak dapat dikompilasi:

cpp Vector constexpr yang hidup melewati evaluasi (tidak dapat dikompilasi)
// vektor-statis-gagal.cpp — vector constexpr yang hidup melewati evaluasi
#include <vector>

constexpr std::vector<int> data = {1, 2, 3};

int main() {}

Pesan nyata dari g++ 14.2:

text
In file included from /usr/include/c++/14/vector:63,
                 from vektor-statis-gagal.cpp:2:
/usr/include/c++/14/bits/allocator.h:193:52: error: ‘std::vector<int>(std::initializer_list<int>{((const int*)(& const int [3]{1, 2, 3})), 3}, std::allocator<int>())’ is not a constant expression because it refers to a result of ‘operator new’
  193 |             return static_cast<_Tp*>(::operator new(__n));
      |                                      ~~~~~~~~~~~~~~^~~~~

Pesan error menyebut operator new: hasil alokasi pada evaluasi konstanta tidak boleh bertahan lebih lama dari evaluasinya. Aturan transient yang sama juga membatasi std::string dan wadah lain yang mengalokasikan memori. Solusinya, simpan hasilnya, bukan wadahnya — kembalikan std::array, nilai tunggal, atau class literal berisi angka siap pakai.

Kaitan dengan if constexpr dan NTTP #

Dua fitur yang sudah kita pelajari terhubung langsung dengan constexpr. Chapter if constexpr pada Section Modern C++ memilih cabang pada saat kompilasi berdasarkan nilai konstanta — dan nilai itu sering datang dari fungsi constexpr, seperti pangkatDua<N>() pada chapter tersebut. Chapter Non-type Template Parameters di section ini menunjukkan arah sebaliknya: konstanta hasil fungsi constexpr menjadi bagian dari tipe, misalnya Buffer<double, ukuran>. Kombinasi keduanya menghasilkan buffer berkapasitas pasti dan template yang berperilaku berbeda per konstanta.

Bedakan pula dua kata kunci yang tampak mirip. if constexpr menanyakan nilai dari kondisinya, sedangkan if consteval (C++23) menanyakan konteks evaluasi — apakah kita sedang dikompilasi atau sedang berjalan; fungsi jalurEksekusi() pada contoh pertama memakai yang kedua. Bila kode perlu berperilaku berbeda antara kedua konteks — misalnya alokasi runtime sebagai jalur cadangan — pasangan ini alat yang tepat.

Sampai di sini kita memiliki dua alat penyaring pada fase kompilasi: type traits yang menjawab pertanyaan tentang tipe, dan fungsi constexpr/consteval yang menghitung nilai. Keduanya akan sering bergandengan pada template berukuran lebih besar. Pertanyaan yang tersisa adalah bagaimana menyatakan kendala tipe secara terbuka dan mudah dibaca — itulah concepts, topik chapter berikutnya.

Referensi resmi: cppreference — constexpr specifier , cppreference — consteval specifier , dan cppreference — if statement (if consteval) .