Template punya masalah klasik: satu badan fungsi harus sah untuk semua tipe yang mungkin dipakai, padahal sering kali tipe tertentu butuh jalur berbeda. Sebelum C++17, kita memaksa solusi tidak enak — overload berlapis (tag dispatch) atau trik std::enable_if bernama SFINAE — hanya supaya compiler mau memilih satu dari beberapa jalur. if constexpr menyelesaikan masalah ini langsung: cabang yang kondisinya gagal saat kompilasi dibuang (discarded), sehingga kode di dalamnya tidak perlu sah untuk tipe yang sedang diproses. Hasilnya, satu template bisa memuat beberapa perilaku sekaligus dengan pilihan yang ditentukan compiler, bukan saat program berjalan.
Cabang yang Dipilih saat Kompilasi #
Sintaksnya persis if biasa ditambah kata constexpr setelahnya, dan kondisinya harus berupa ekspresi konstanta. Program berikut menunjukkan tiga pola pemakaian yang paling sering muncul: perilaku per tipe lewat type traits, pemilihan jalur berdasarkan kategori iterator, dan penghentian rekursi template.
// if-constexpr.cpp — if constexpr: cabang yang dipilih saat kompilasi
// Kompilasi: g++ -std=c++23 -Wall -Wextra if-constexpr.cpp -o if-constexpr
#include <cmath>
#include <list>
#include <print>
#include <string>
#include <type_traits>
#include <vector>
// Satu template, tiga perilaku: cabang yang tidak dipilih DIBUANG
template <typename T>
void periksa(const T& nilai) {
if constexpr (std::is_integral_v<T>) {
std::print("tipe integral: {} (dua kali = {})\n", nilai, nilai * 2);
} else if constexpr (std::is_floating_point_v<T>) {
std::print("tipe pecahan : {:.2f} (dibulatkan = {})\n",
nilai, std::round(nilai));
} else {
std::print("tipe lain : \"{}\" (panjang = {})\n", nilai, nilai.size());
}
}
// Menggantikan tag dispatch: satu fungsi untuk semua kategori iterator
template <typename It>
void majukan(It& it, int n) {
using Kategori = typename std::iterator_traits<It>::iterator_category;
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, Kategori>) {
it += n; // iterator acak: lompat langsung
} else {
for (int i = 0; i < n; ++i) { // iterator lain: maju selangkah
++it;
}
}
}
// Rekursi template berhenti rapi tanpa spesialisasi terpisah
template <unsigned N>
constexpr unsigned long long pangkatDua() {
if constexpr (N == 0) {
return 1;
} else {
return 2 * pangkatDua<N - 1>();
}
}
int main() {
periksa(42);
periksa(3.7);
periksa(std::string("modern")); // panggilan .size() hanya sah di cabang ini
std::vector<int> v{10, 20, 30, 40, 50};
auto iv = v.begin();
majukan(iv, 3);
std::print("vector maju 3: {}\n", *iv);
std::list<int> l{10, 20, 30, 40, 50};
auto il = l.begin();
majukan(il, 3);
std::print("list maju 3 : {}\n", *il);
std::print("2^10 = {}\n", pangkatDua<10>());
static_assert(pangkatDua<8>() == 256, "2^8 dihitung saat kompilasi");
return 0;
}Output yang Dihasilkan:
tipe integral: 42 (dua kali = 84)
tipe pecahan : 3.70 (dibulatkan = 4)
tipe lain : "modern" (panjang = 6)
vector maju 3: 40
list maju 3 : 40
2^10 = 1024Mari kita bedah:
- Baris 12-22 (periksa): template menerima tipe apa pun, tetapi
if constexprmemeriksastd::is_integral_v<T>danstd::is_floating_point_v<T>— dua type traits yang nilainya diketahui saat kompilasi. Untukint, hanya cabang pertama yang tersisa; untukdouble, hanya cabang kedua. - Baris 15 vs 20: inilah kuncinya —
nilai * 2sah untukinttetapi tidak untukstd::string, dannilai.size()kebalikannya. Jika kita memakaiifbiasa, compiler menolak kedua baris ini karena setiap cabang harus dikompilasi untuk semua tipe. Denganif constexpr, compiler hanya mengkompilasi cabang yang kondisinya benar. - Baris 27-34 (majukan):
iterator_traitsmengambil kategori iterator (Kategori);is_base_of_vmengecek apakah kategori itu turunanrandom_access_iterator_tag.std::vectorlolos uji ini sehingga memakaiit += n;std::list(bidirectional) tidak lolos dan memakai loop++it. - Baris 62-63:
pangkatDua<10>()dievaluasi jadi konstanta — hasilnya bahkan bisa diverifikasi lewatstatic_asserttanpa menjalankan program, bukti paling jelas bahwa cabangnya dipilih saat kompilasi.
Perbedaan dengan if Biasa #
Untuk melihat kenapa if constexpr diperlukan, coba tulis ulang majukan sebagai template dengan if biasa yang selalu memanggil it += n:
template <typename It>
void majukanLama(It& it, int n) {
it += n; // list iterator tidak punya operator+=
}
// pemanggilan majukanLama(il, 2) dengan list iterator menghasilkan:
probe3.cpp:7:8: error: no match for 'operator+='
(operand types are 'std::_List_iterator<int>' and 'int')
7 | it += n;
| ~~~^~~~Kegagalan ini muncul karena template diinstansiasi untuk std::list<int>::iterator, dan badan fungsinya memuat it += n yang tidak ada di tipe itu. if constexpr menghindari jebakan ini: pada saat compiler mengetahui It adalah iterator list, cabang it += n sudah dibuang sehingga baris tersebut tidak pernah diinstansiasi. Pesan error persisnya memang berbeda antar versi compiler, tetapi intinya selalu sama — operator yang dicari tidak tersedia untuk tipe yang diinstansiasi.
Menggantikan Tag Dispatch dan SFINAE #
Sebelum ada if constexpr, pilihan perilaku per tipe dilakukan dengan dua pola yang verbose. Pola pertama, tag dispatch, membuat tipe kosong (tag) sebagai parameter tambahan lalu menyerahkan pemilihan ke resolusi overload:
struct AksesAcak {}; // tag untuk iterator acak
struct AksesBertahap {}; // tag untuk iterator lain
template <typename It>
void majukanImpl(It& it, int n, AksesAcak) { it += n; }
template <typename It>
void majukanImpl(It& it, int n, AksesBertahap) { for (int i = 0; i < n; ++i) ++it; }
template <typename It>
void majukan(It& it, int n) {
// pilih tag sesuai kategori, lalu diteruskan ke overload yang cocok
}Pola kedua, SFINAE, menyembunyikan fungsi lewat std::enable_if_t pada tipe kembalian atau parameter default — rumit ditulis, lebih rumit dibaca, dan pesan errornya legendaris panjangnya. Kedua pola kini dapat digantikan oleh satu fungsi dengan if constexpr seperti majukan pada contoh utama: kondisi type traits ditulis sekali, cabang-cabangnya terbaca berurutan, dan tidak ada helper tambahan. Untuk kebutuhan validasi tipe pada API publik (memberi pesan error yang rapi ketika tipe tidak cocok), gunakan concepts — topik chapter berikutnya, yang justru bekerja paling mulus ketika dipadukan dengan if constexpr.
Menghentikan Rekursi Template #
Template rekursif seperti pangkatDua<N> sebelumnya membutuhkan dua definisi: template utama untuk kasus umum dan spesialisasi untuk kasus dasar N == 0. Dengan if constexpr, kedua kasus hidup dalam satu fungsi — saat N == 0, cabang else yang memanggil pangkatDua<N - 1>() dibuang, sehingga rekursi benar-benar berhenti dan compiler tidak pernah mencoba menginstansiasi pangkatDua dengan parameter yang makin kecil tanpa ujung. Pola ini berlaku umum untuk semua meta-programming berbasis template: kondisi dasar ditulis sebagai cabang if constexpr, kasus rekursif sebagai cabang pasangannya, tanpa boilerplate spesialisasi.
Kombinasi if constexpr + type traits adalah alat sehari-hari pengguna C++ modern: sekarang kita tahu cara memilih jalur per tipe di dalam template. Chapter berikutnya memperluas alat ini ke level API: concepts untuk menyatakan syarat tipe secara eksplisit dan ranges untuk algoritma serta view yang tervalidasi.
Rujukan resmi: cppreference — if constexpr , cppreference — type traits .