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.
// 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:
faktorial(6) = 720 (dihitung saat kompilasi)
faktorial(7) = 5040 (di konteks biasa)
faktorial(8) = 40320 (runtime)
jalurEksekusi() saat runtime = 0Bedah baris demi baris:
- Baris 8 (
constexpr int faktorial(int n)): kualifikasiconstexprmenandai fungsi; loop dan variabel lokal di dalamnya sah sejak C++14, sementara standar C++11 menuntut satureturntunggal. - Baris 17-23 (
jalurEksekusi):if consteval(C++23) memilih cabang berdasarkan konteks evaluasi; pendahulunya pada C++20 adalahstd::is_constant_evaluated()dari<type_traits>. - Baris 27-29 (
static_assert(...)): tiga pemeriksaan yang wajib lulus pada saat kompilasi;jalurEksekusi() == 1di baris 29 membuktikan kita sedang di jalur kompilasi. - Baris 33 (
constexpr int konstEnam = faktorial(6)): inisialisasi variabelconstexprmenuntut constant expression, sehingga compiler wajib menghitungfaktorial(6). - Baris 37 (
faktorial(7)): konteks biasa dengan argumen konstanta — standar tidak mewajibkan dan tidak melarang evaluasi compile time; jaminannya hanya hasil yang identik. - Baris 41-42 (
volatile int masukan):volatilemelarang compiler memperlakukan variabel sebagai konstanta, sehingga pemanggilan pasti berjalan saat runtime — danjalurEksekusi()tercetak0.
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 pemanggilan | Contoh | Kapan dievaluasi |
|---|---|---|
Kondisi static_assert | static_assert(faktorial(5) == 120) | wajib saat kompilasi |
Inisialisasi variabel constexpr | constexpr int c = faktorial(6) | wajib saat kompilasi |
| Argumen NTTP / ukuran array | std::array<int, faktorial(4)> | wajib saat kompilasi |
| Konteks biasa, argumen konstanta | std::cout << faktorial(7) | bebas — hasilnya tetap identik |
| Ada argumen non-konstanta | faktorial(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.
// 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:
kuadrat(4) = 16
ukuran array = 9
kuadrat(12) = 144
perkalianFaktorial(3, 5) = 720Bedah baris demi baris:
- Baris 9 (
consteval int kuadrat(int n)): deklarasi immediate function; tidak ada kataconstexprdi depannya meskipun semantiknya menurun dari fungsi constexpr. - Baris 22-24 (
perkalianFaktorial): fungsi consteval boleh memanggil fungsiconstexpr; di dalam konteks immediate function, hasil panggilan itu ikut dihitung pada saat kompilasi. - Baris 27 (
static_assert): bukti compile time untukkuadrat(9). - Baris 31 (
constexpr int sisi = kuadrat(4)): konteks yang menuntut konstanta cocok dengan janji consteval. - 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. - Baris 40-41 (
std::cout << kuadrat(12)): pemanggilan pada konteks runtime tetap sah karena12dan pasangan3, 5adalah 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.
// 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:
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.
// 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:
rata-rata nilai = 77
jumlah spasi = 4Bedah baris demi baris:
- Baris 12-17 (
rataRata):std::vectorlokal dibuat dan masa hidupnya berakhir di dalam satu evaluasi konstanta — persis bentuk alokasi transient yang diizinkan. - Baris 14 (
std::sort): algoritma constexpr sejak C++20; pengurutan lima angka terjadi pada fase kompilasi. - Baris 15 (
std::accumulate): algoritma dari<numeric>juga constexpr sejak C++20. - Baris 20-23 (
hitungSpasi):std::stringconstexpr sejak C++20; string sementara dibangun lalu selesai di dalam evaluasi. - Baris 26-27 (
static_assert): bukti compile time untuk kedua fungsi; pemanggilan ulang dimainmenghasilkan 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:
// 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:
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) .