Type trait

Introspeksi tipe saat kompilasi dengan <type_traits>.

Daftar isi

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.

TraitPertanyaan yang dijawab
std::is_integral_vApakah tipenya bilangan bulat?
std::is_floating_point_vApakah tipenya pecahan?
std::is_arithmetic_vApakah tipenya bulat atau pecahan?
std::is_pointer_vApakah tipenya pointer?
std::is_reference_vApakah tipenya referensi?
std::is_enum_vApakah tipenya enum?
std::is_class_vApakah tipenya struct atau class?
std::is_same_vApakah 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.

cpp trait.cpp
#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;
}
text Keluaran
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 satu

Fungsi 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.

cpp kondisi.cpp
#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;
}
text Keluaran
Simpanan<double> bertipe double : true
Simpanan<int> bertipe long long : true
simpan(3.5) : 3.5
simpan(7)   : 7
gabungan trait terpenuhi

Alias 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.

cpp Menuntut tipe integral pada template
// 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 konstanta true atau false tentang sifat sebuah tipe pada saat kompilasi.
  • Akhiran _v memanggil member ::value secara ringkas, dan std::decay_t membersihkan referensi serta kualifikasi tipe sebelum diuji.
  • if constexpr membuang cabang yang tidak terpilih sejak kompilasi, sehingga kode khusus tipe dapat ditulis dalam satu template.
  • static_assert berbasis trait mengubah pelanggaran kontrak tipe menjadi error kompilasi yang jelas.
  • std::conditional_t memilih tipe pada saat kompilasi, dan beberapa trait dapat digabungkan dengan operator logika biasa.

Referensi#