Topik ini membahas if constexpr, yaitu percabangan yang keputusannya diambil saat kompilasi. Pembahasan mencakup kondisi konstanta yang menjadi syaratnya, cabang yang terbuang dan tidak diinstansiasi, serta bagaimana fitur ini membersihkan kode template dari pola tag dispatch dan SFINAE. Contohnya berupa program utuh yang memperlihatkan satu template melayani beberapa kelompok tipe dengan tiga cabang berbeda.
Masalah satu template dengan perilaku berbeda#
Sebuah template sering kali perlu berperilaku berbeda untuk kelompok tipe yang berbeda, misalnya mengalikan bilangan bulat dengan dua, membagi pecahan dengan dua, dan sekadar mencetak tipe lainnya. Sebelum C++17, kebutuhan semacam ini dijawab dengan tag dispatch, yaitu menyiapkan beberapa overloading dan memilihnya lewat argumen tambahan bertipe trait. Pola semacam ini bekerja, tetapi satu perilaku terpecah ke beberapa fungsi dan butuh helper tambahan yang tidak berhubungan dengan logika utamanya.
// tidak-dijalankan (pola lama untuk perbandingan)
template <typename T>
void prosesNilai(const T& nilai, std::true_type) {
// jalur untuk tipe bilangan bulat
}
template <typename T>
void prosesNilai(const T& nilai, std::false_type) {
// jalur untuk tipe lain
}
template <typename T>
void prosesNilai(const T& nilai) {
prosesNilai(nilai, std::is_integral<T>{});
}Dua overload pertama menerima argumen tambahan yang tipenya ditentukan trait std::is_integral, sedangkan overload ketiga meneruskan std::true_type atau std::false_type sesuai hasilnya. Pemilihan overload mengarahkan eksekusi ke jalur yang tepat, tetapi argumen tambahan itu hanyalah alat pemilih yang tidak berpartisipasi dalam perhitungan. if constexpr menghapus seluruh perantara tersebut.
Percabangan saat kompilasi#
if constexpr memeriksa kondisi yang berupa ekspresi konstanta saat kompilasi, dan hanya cabang yang terpilih yang diinstansiasi. Kondisinya biasanya berupa type trait seperti std::is_integral_v<T> yang hasilnya sudah diketahui sebelum program berjalan. Bentuk else if constexpr dapat disambung berlapis, sedangkan cabang else biasa menampung sisanya.
#include <iostream>
#include <string>
#include <type_traits>
template <typename T>
void prosesNilai(T nilai) {
if constexpr (std::is_integral_v<T>) {
std::cout << "Bilangan bulat, dikali dua: " << nilai * 2 << "\n";
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "Pecahan, dibagi dua: " << nilai / 2 << "\n";
} else {
std::cout << "Tipe lain: " << nilai << "\n";
}
}
int main() {
prosesNilai(21);
prosesNilai(7.5);
prosesNilai(std::string("catatan"));
return 0;
}Bilangan bulat, dikali dua: 42
Pecahan, dibagi dua: 3.75
Tipe lain: catatanKetiga pemanggilan memakai satu template yang sama, dan tiap tipe menerima cabang yang sesuai dengan kelompoknya. int masuk cabang pertama sehingga 21 menjadi 42, double masuk cabang kedua sehingga 7.5 menjadi 3.75, dan std::string masuk cabang terakhir dan dicetak apa adanya. Seluruh pemilihan selesai saat kompilasi, sehingga program yang berjalan hanya memuat instruksi cabang yang relevan untuk tipe yang dipakai.
Cabang yang terbuang tidak diinstansiasi#
Keunggulan paling menentukan dari if constexpr adalah cabang yang tidak terpilih tidak diinstansiasi. Program berikut mengembalikan sizeof(T) untuk tipe bilangan bulat dan nilai.size() untuk tipe lainnya. Bagian nilai.size() mustahil dikompilasi untuk int, tetapi pemanggilan panjangIsi(42) tetap berhasil karena cabang tersebut terbuang untuk T bertipe int.
#include <cstddef>
#include <iostream>
#include <string>
#include <type_traits>
template <typename T>
std::size_t panjangIsi(const T& nilai) {
if constexpr (std::is_integral_v<T>) {
return sizeof(T);
} else {
return nilai.size();
}
}
int main() {
std::cout << panjangIsi(42) << "\n";
std::cout << panjangIsi(std::string("template")) << "\n";
return 0;
}4
8Keluaran 4 adalah ukuran int dalam byte pada platform ini, sedangkan 8 adalah panjang "template". Perbandingannya dengan if biasa menegaskan bedanya. Kondisi if biasa boleh berupa nilai saat program berjalan, tetapi kedua cabangnya tetap diinstansiasi. Karena itu, nilai.size() untuk int tetap ditolak compiler dengan pesan request for member 'size' in 'nilai' meskipun cabangnya tidak pernah dijalankan.
// tidak-dijalankan (contoh error kompilasi)
template <typename T>
std::size_t panjangIsi(const T& nilai) {
if (std::is_integral_v<T>) { // hasilnya true untuk int
return sizeof(T);
} else {
return nilai.size(); // ditolak untuk T = int
}
}Kombinasi dengan template variadic#
Template variadic memperjelas kegunaan if constexpr, karena keputusan per tipe perlu diambil untuk sejumlah argumen yang tidak diketahui sebelumnya. Fungsi berat di bawah menilai satu nilai menurut kelompok tipenya, sedangkan totalBerat melipat seluruh argumennya dengan fold expression. Setiap instansiasi berat hanya memuat satu cabang sesuai tipenya, sehingga penjumlahan berlangsung tanpa pemeriksaan tipe saat program berjalan. Bentuk yang sama berlaku untuk pemrosesan apa pun yang keputusannya bergantung pada tipe tiap argumen, misalnya mencatat hanya angka dan mengabaikan tipe lain.
#include <iostream>
#include <string>
#include <type_traits>
template <typename T>
int berat(const T& nilai) {
if constexpr (std::is_integral_v<T>) {
return 2;
} else if constexpr (std::is_floating_point_v<T>) {
return 1;
} else {
return 0;
}
}
template <typename... Args>
int totalBerat(const Args&... args) {
return (berat(args) + ... + 0);
}
int main() {
std::cout << totalBerat(1, 2.5, std::string("teks"), 3) << "\n";
return 0;
}5Hasil 5 berasal dari 2 untuk dua argumen bilangan bulat, 1 untuk pecahan, dan 0 untuk std::string. Tanpa if constexpr, kombinasi kelompok tipe semacam ini menuntut overloading helper untuk setiap kelompok, dan jumlah helpernya bertambah setiap kali kelompok baru diperlukan. Satu fungsi dengan percabangan konstanta menampung seluruhnya di satu tempat yang mudah dibaca, sekaligus menjaga keputusan tipe tetap berhenti pada saat kompilasi.
Pemakaian di luar template#
Percabangan if constexpr tidak terbatas pada template. Kondisinya tetap wajib berupa ekspresi konstanta, sehingga bentuk ini dapat memilih kode berdasarkan variabel constexpr di dalam fungsi biasa. Di luar template tidak ada konstruksi yang bergantung pada parameter tipe, sehingga kedua cabang tetap diperiksa utuh oleh compiler dan manfaat cabang terbuang tidak berlaku. Pemakaiannya lebih sempit dibanding pada template, tetapi tetap berguna untuk mengaktifkan jalur pelacakan atau pemeriksaan tambahan yang ditentukan saat kompilasi.
#include <iostream>
constexpr bool modeJejak = false;
int main() {
if constexpr (modeJejak) {
std::cout << "mode jejak aktif\n";
} else {
std::cout << "mode normal\n";
}
return 0;
}mode normalMengubah modeJejak menjadi true mengaktifkan jalur pelacakan tanpa menyentuh badan fungsinya. Jalurnya dipilih sebelum program berjalan, sehingga tidak ada pemeriksaan kondisi pada saat eksekusi. Bentuk ini setara dalam hasil dengan if biasa atas konstanta yang sama. Perbedaannya, kondisi if constexpr dijamin dinilai saat kompilasi, sehingga kondisi yang nilainya baru diketahui saat program berjalan ditolak compiler.
Kesalahan umum#
Tiga kekeliruan berikut paling sering muncul saat memakai if constexpr.
- Memakai kondisi yang nilainya baru diketahui saat program berjalan, misalnya hasil bacaan berkas.
if constexprmenuntut ekspresi konstanta, sehingga compiler menolak kondisi semacam itu. Gunakanifbiasa untuk keputusan saat berjalan. - Menganggap seluruh badan fungsi bebas diperiksa. Cabang yang terpilih tetap diinstansiasi penuh, sehingga operasi yang salah pada cabang yang benar tetap dilaporkan. Pastikan cabang yang dipakai untuk
Tmemang valid. - Menuliskan
if constexpruntuk keputusan yang sebenarnya berjalan, misalnya memilih langkah berdasarkan data pengguna. Percabangan semacam itu seharusnyaifbiasa, danif constexpryang disalahtempatkan membuat kode terlihat dihitung saat kompilasi padahal tidak.
Ringkasan#
if constexprmemeriksa kondisi berupa ekspresi konstanta saat kompilasi, dan hanya cabang yang terpilih yang diinstansiasi.- Cabang yang terbuang tidak diinstansiasi, sehingga satu template dapat memuat operasi yang hanya valid untuk sebagian tipenya.
- Fitur ini menggantikan tag dispatch dan sebagian besar SFINAE, sehingga perilaku per kelompok tipe kembali berada dalam satu fungsi.
- Dikombinasikan dengan fold expression, satu template variadic dapat mengambil keputusan tipe untuk setiap argumennya saat kompilasi.
Referensi#
- if statement — cppreference — Aturan if constexpr dan semantik cabang yang terbuang.
- C++ Standards Support in GCC — GCC — Status dukungan fitur C++17 seperti if constexpr pada GCC.
- Constant expressions — LearnCpp — Penjelasan ekspresi konstanta yang menjadi kondisi if constexpr.