Topik ini membahas type trait, yaitu template dari header <type_traits> yang menjawab pertanyaan tentang tipe pada saat kompilasi. Pembahasan mencakup trait dasar seperti std::is_integral, pembersihan tipe dengan std::decay_t, percabangan if constexpr berbasis trait, dan kontrak tipe menggunakan static_assert. Setelah membaca topik ini, pembaca dapat menulis template yang menyusun ulang perilakunya menurut tipe argumen tanpa menunggu program berjalan.
Trait sebagai pertanyaan tentang tipe#
Sebuah type trait adalah template yang menghasilkan konstanta true atau false menurut sifat tipe yang diujinya. Trait std::is_integral_v<T>, misalnya, bernilai true untuk int, long, dan char, tetapi false untuk double dan std::string. Trait lain menguji persamaan tipe (std::is_same_v), keberadaan pointer (std::is_pointer_v), atau sifat referensi (std::is_reference_v). Karena hasilnya berupa konstanta, jawaban tersebut terpaku sejak kompilasi dan tidak memakan memori maupun waktu saat program berjalan.
Trait pada header <type_traits> dikelompokkan menurut pertanyaannya, dan beberapa yang paling sering dipakai tercantum di bawah ini.
| Trait | Pertanyaan yang dijawab |
|---|---|
std::is_integral_v | Apakah tipenya bilangan bulat? |
std::is_floating_point_v | Apakah tipenya pecahan? |
std::is_arithmetic_v | Apakah tipenya bulat atau pecahan? |
std::is_pointer_v | Apakah tipenya pointer? |
std::is_reference_v | Apakah tipenya referensi? |
std::is_enum_v | Apakah tipenya enum? |
std::is_class_v | Apakah tipenya struct atau class? |
std::is_same_v | Apakah dua tipe itu identik? |
Kelompok lain melengkapi daftar tersebut, misalnya std::is_const_v untuk kualifikasi dan std::is_move_constructible_v untuk kemampuan memindahkan nilai.
Akhiran _v adalah singkatan C++17 dari member statis ::value, sehingga std::is_integral_v<T> setara dengan std::is_integral<T>::value yang lebih panjang. Program berikut menguji beberapa tipe melalui satu fungsi template, dan hasilnya dicetak dengan std::boolalpha agar tampil sebagai true dan false.
#include <iostream>
#include <string>
#include <type_traits>
template <typename T>
void periksa(const T&) {
std::cout << "integral: " << std::is_integral_v<T>
<< ", floating-point: " << std::is_floating_point_v<T> << '\n';
}
int main() {
std::cout << std::boolalpha;
periksa(42);
periksa(3.14);
periksa(std::string{"terbilang"});
std::cout << "int dan long sama: " << std::is_same_v<int, long> << '\n';
auto tampil = [](const auto& nilai) {
// decay_t melepas referensi dan const, sehingga trait membaca tipe dasarnya
using T = std::decay_t<decltype(nilai)>;
if constexpr (std::is_integral_v<T>) {
std::cout << "dua kali lipat: " << nilai * 2 << '\n';
} else {
std::cout << "cetak apa adanya: " << nilai << '\n';
}
};
tampil(21);
tampil(std::string("dua puluh satu"));
return 0;
}integral: true, floating-point: false
integral: false, floating-point: true
integral: false, floating-point: false
int dan long sama: false
dua kali lipat: 42
cetak apa adanya: dua puluh satuFungsi periksa menerima 42 sebagai int dan 3.14 sebagai double, sedangkan std::string{"terbilang"} menghasilkan std::string. Ketiganya dilayani template yang sama, tetapi trait di dalamnya melaporkan sifat tipe masing-masing tanpa perlu overload manual.
Pembersihan tipe dengan decay_t#
Baris tampil(21) di atas menyembunyikan satu jebakan yang penting dipahami. Parameter lambda generik ditulis const auto&, sehingga decltype(nilai) menghasilkan const int&, yaitu tipe referensi, dan bukan int. Trait std::is_integral_v pada tipe referensi menghasilkan false, sehingga percabangan yang diharapkan tidak pernah terpilih.
std::decay_t<T> membersihkan tipe dari lapisan tersebut: referensi dilepas, const dan volatile dibuang, array dikonversi menjadi pointer, dan fungsi dikonversi menjadi pointer fungsi. Hasilnya adalah tipe dasar, dan tipe inilah yang diuji trait agar hasil pemeriksaannya andal. Pada program sebelumnya, using T = std::decay_t<decltype(nilai)> membuat is_integral_v<T> membaca int dengan benar. Trait pendamping lain seperti std::remove_cv_t dan std::remove_reference_t membersihkan lapisan secara lebih selektif apabila decay yang lengkap terlalu jauh.
Cabang kompilasi dengan if constexpr#
Trait menjadi sangat berguna ketika dipasangkan dengan if constexpr, yaitu percabangan yang cabang tidak terpilihnya dibuang sejak kompilasi. Pada fungsi tampil, cabang nilai * 2 hanya ditulis untuk tipe integral. Apabila percabangan ini ditulis sebagai if biasa, ekspresi std::string * 2 tetap harus valid bagi compiler dan program gagal dikompilasi, karena kedua cabang diperiksa tipe. Dengan if constexpr, cabang yang tidak terpilih dibuang, sehingga kode yang hanya sah untuk satu kelompok tipe dapat hidup berdampingan dalam satu template.
Sebelum C++17, kebutuhan serupa dilayani oleh teknik std::enable_if dan SFINAE yang menonaktifkan overload secara halus. Teknik itu masih ada pada kode lama, tetapi if constexpr memberi hasil serupa dengan pembacaan yang jauh lebih mudah. Pada C++20, concept menawarkan bentuk yang lebih deklaratif lagi untuk membatasi template.
Memilih tipe dengan std::conditional_t#
if constexpr memilih cabang pernyataan, sedangkan std::conditional_t memilih tipe pada saat kompilasi yang sama. Trait ini menerima syarat trait, tipe untuk syarat yang benar, dan tipe untuk syarat yang salah, lalu membentuk alias tipe hasilnya. Alias semacam ini sering dipakai untuk menentukan tipe penyimpanan atau tipe hasil fungsi bantu tanpa menulis spesialisasi template. Bentuk dengan akhiran _t inilah yang dipakai sehari-hari, karena std::conditional tanpa akhiran menghasilkan struct yang tipe hasilnya perlu diakses melalui member type.
#include <iostream>
#include <type_traits>
// Memilih tipe penyimpanan: pecahan disimpan sebagai double, selainnya long long.
template <typename T>
using Simpanan = std::conditional_t<std::is_floating_point_v<T>, double, long long>;
template <typename T>
Simpanan<T> simpan(T nilai) {
return static_cast<Simpanan<T>>(nilai);
}
int main() {
std::cout << std::boolalpha;
std::cout << "Simpanan<double> bertipe double : "
<< std::is_same_v<Simpanan<double>, double> << '\n';
std::cout << "Simpanan<int> bertipe long long : "
<< std::is_same_v<Simpanan<int>, long long> << '\n';
std::cout << "simpan(3.5) : " << simpan(3.5) << '\n';
std::cout << "simpan(7) : " << simpan(7) << '\n';
// Trait dapat digabungkan dengan operator logika biasa.
static_assert(std::is_integral_v<int> && !std::is_same_v<int, bool>);
std::cout << "gabungan trait terpenuhi\n";
return 0;
}Simpanan<double> bertipe double : true
Simpanan<int> bertipe long long : true
simpan(3.5) : 3.5
simpan(7) : 7
gabungan trait terpenuhiAlias Simpanan<double> terbentuk menjadi double, sedangkan Simpanan<int> menjadi long long, dan fungsi simpan mengembalikan tipe sesuai argumennya tanpa overload manual. Baris static_assert memperlihatkan cara menggabungkan beberapa trait dengan operator && dan !, yaitu logika biasa yang dihitung saat kompilasi. Pola penyusunan trait semacam ini menjadikan pertanyaan sederhana sebagai bahan baku syarat yang lebih kompleks. Untuk pilihan yang melibatkan lebih dari dua kemungkinan, std::conditional_t dapat disarangkan, tetapi pada titik itu spesialisasi template biasanya lebih mudah dibaca.
Kontrak tipe dengan static_assert#
Trait juga dipakai sebagai penjaga kontrak pada template melalui static_assert. Apabila syarat trait tidak terpenuhi, kompilasi berhenti dengan pesan yang ditulis sendiri, alih-alih gagal di dalam instansiasi yang pesannya panjang dan sulit diikuti.
// tidak-dijalankan (fragmen; butuh pemanggil template agar terinstansiasi)
#include <type_traits>
#include <vector>
template <typename T>
unsigned int jumlah_byte(const std::vector<T>& data) {
static_assert(std::is_integral_v<T>, "jumlah_byte hanya menerima tipe integral");
return data.size() * sizeof(T);
}Panggilan jumlah_byte dengan std::vector<int> lolos, sedangkan pemanggilan dengan std::vector<std::string> menghentikan kompilasi tepat pada baris static_assert beserta pesannya. Pola ini memindahkan kesalahan dari saat program berjalan ke saat kompilasi, kelompok kesalahan yang paling murah untuk diperbaiki. Kombinasi trait, if constexpr, dan static_assert membentuk introspeksi tipe yang lengkap tanpa alat tambahan apa pun di luar pustaka standar.
Posisi static_assert bebas sebagaimana deklarasi lain, sehingga pemeriksaan dapat ditempatkan di dalam badan fungsi, di dalam definisi kelas, maupun pada lingkup global. Pemeriksaan pada lingkup global cocok untuk invarian yang berlaku lintas berkas, misalnya menuntut ukuran tertentu pada struktur data yang dibagikan antar komponen. Apabila syaratnya gagal, compiler menunjuk baris static_assert beserta pesannya, sehingga penyebabnya lebih cepat ditemukan dibandingkan error yang muncul dari dalam rantai instansiasi template.
Ringkasan#
- Type trait dari
<type_traits>menghasilkan konstantatrueataufalsetentang sifat sebuah tipe pada saat kompilasi. - Akhiran
_vmemanggil member::valuesecara ringkas, danstd::decay_tmembersihkan referensi serta kualifikasi tipe sebelum diuji. if constexprmembuang cabang yang tidak terpilih sejak kompilasi, sehingga kode khusus tipe dapat ditulis dalam satu template.static_assertberbasis trait mengubah pelanggaran kontrak tipe menjadi error kompilasi yang jelas.std::conditional_tmemilih tipe pada saat kompilasi, dan beberapa trait dapat digabungkan dengan operator logika biasa.
Referensi#
- Type traits — cppreference.com
— Daftar lengkap trait pada header
<type_traits>beserta kategorinya. - If statement — cppreference.com
— Aturan
if constexprdan pembuangan cabang pada saat kompilasi. - static_assert declaration — cppreference.com — Aturan pemeriksaan konstanta pada saat kompilasi.