auto, decltype, dan structured binding

Kotak alat deklarasi modern dan trailing return type.

Daftar isi

Topik ini membahas kotak alat deklarasi modern C++, yaitu auto, decltype, trailing return type, dan structured binding dari C++17. Keempatnya menentukan tipe pada saat kompilasi sehingga penulisan tipe panjang dapat dihindari tanpa kehilangan keamanan tipe. Setelah membaca topik ini, pembaca memahami kapan compiler menentukan tipe secara otomatis, bagaimana menanyakan tipe suatu ekspresi, dan cara membongkar pasangan nilai menjadi nama-nama yang bermakna.

auto untuk inferensi tipe#

auto menyuruh compiler menentukan tipe variabel dari penginisialisasinya, teknik yang disebut type deduction. Aturannya sama dengan penentuan tipe parameter template, sehingga auto melepaskan referensi dan kualifikasi const tingkat atas. Apabila sumber nilainya adalah const std::string&, maka auto x = sumber; menghasilkan std::string berupa salinan baru. Untuk menghindari penyalinan yang tak perlu, bentuk yang dipilih adalah auto& atau const auto&.

Inferensi ini paling terasa pada tipe yang panjang, misalnya iterator dan hasil container. Baris auto posisi = stok.find("pensil"); jauh lebih ringkas daripada menulis std::map<std::string, int>::iterator secara manual, padahal keduanya bertipe sama persis. Tipe hasilnya tetap diperiksa compiler, sehingga kesalahan pemakaian tetap terdeteksi sebagaimana deklarasi eksplisit.

Satu nuansa yang perlu dijaga adalah interaksi auto dengan kurung kurawal. Deklarasi auto jumlah = {10, 20} menyimpulkan std::initializer_list<int>, bukan array maupun std::vector, sedangkan deklarasi auto satu{10} pada C++17 menyimpulkan int biasa. Bentuk kurawalnya mirip, tetapi hasilnya berbeda, sehingga inisialisasi yang memang menyasar initializer_list lebih baik ditulis dengan tipe lengkapnya agar maksudnya tidak rancu.

decltype dan perbedaannya dengan auto#

decltype menjawab pertanyaan kebalikan dari auto: alih-alih menentukan tipe dari nilai, decltype mengambil tipe yang sudah dideklarasikan atau yang dihasilkan sebuah ekspresi, tanpa mengevaluasi ekspresi itu. Bedanya dengan auto terletak pada perlakuannya terhadap referensi. decltype(x) mempertahankan referensi dan const sebagaimana adanya, sedangkan auto melepaskannya. Program berikut membandingkan keduanya sekaligus memperkenalkan trailing return type pada template.

cpp deklarasi.cpp
#include <iostream>
#include <map>
#include <string>
#include <type_traits>

// Trailing return type: tipe hasil bergantung pada tipe parameter template.
template <typename A, typename B>
auto tambah(A a, B b) -> decltype(a + b) {
    return a + b;
}

int main() {
    auto jumlah = tambah(2, 3.5);
    std::cout << std::boolalpha
              << "jumlah bertipe double: "
              << std::is_same_v<decltype(jumlah), double> << '\n';

    int a = 1;
    int& r = a;
    auto b = r;         // b int: salinan biasa
    decltype(r) c = a;  // c int&: alias untuk a
    c = 99;
    std::cout << "a setelah c diubah: " << a << '\n';
    std::cout << "b adalah referensi: " << std::is_reference_v<decltype(b)> << '\n';

    std::map<std::string, int> stok{{"pensil", 24}, {"buku", 10}};
    for (const auto& [nama, banyak] : stok) {
        std::cout << nama << ": " << banyak << '\n';
    }

    auto [posisi, berhasil] = stok.insert({"pensil", 99});
    std::cout << "insert ganda berhasil: " << berhasil << '\n';
    std::cout << "stok pensil tetap: " << posisi->second << '\n';
    return 0;
}
text Keluaran
jumlah bertipe double: true
a setelah c diubah: 99
b adalah referensi: false
buku: 10
pensil: 24
insert ganda berhasil: false
stok pensil tetap: 24

Baris auto b = r; menghasilkan int yang berdiri sendiri, terbukti dari nilai a yang tidak berubah ketika b diisi. Sebaliknya decltype(r) c = a; menghasilkan int&, sehingga c = 99 langsung mengubah a. Kedua deklarasi hanya berbeda satu kata, tetapi maknanya berbeda sepenuhnya. Pola decltype(...) sering dipakai bersama std::decay_t untuk mendapatkan tipe dasarnya, sebagaimana dibahas pada topik type trait.

Detail kecil yang sering menimbulkan kejutan adalah tanda kurung ganda pada decltype. Untuk variabel int a, ekspresi decltype(a) menghasilkan tipe deklarasinya, yaitu int, sedangkan decltype((a)) memperlakukan a sebagai ekspresi lvalue dan menghasilkan int&. Satu tanda kurung saja mengubah tipe hasilnya, sehingga pola yang mengandalkan referensi yang dipertahankan harus ditulis dengan teliti. Kesalahan semacam ini mudah muncul karena kedua bentuk tersebut sama-sama sah dan tidak memberi peringatan apa pun.

Trailing return type#

Pada fungsi template tambah, tipe hasil tidak dapat ditulis di depan karena bergantung pada A dan B yang baru dikenal setelah daftar parameter. Bentuk auto tambah(A a, B b) -> decltype(a + b) menempatkan tipe hasil setelah panah, yaitu pola trailing return type. Kata auto di depan berarti tipe hasil ditentukan dari bagian belakang, dan decltype(a + b) menghitung tipe tersebut dari parameter yang sudah tersedia.

Bentuk ini juga dipakai tanpa template apabila tipe hasilnya panjang atau dihitung dari ekspresi, misalnya auto rasio(double pembilang, double penyebut) -> double. Sejak C++14, kata kembalian auto tunggal pada fungsi biasa sudah cukup dan compiler menyimpulkan tipenya dari return, tetapi bentuk trailing tetap diperlukan apabila tipe hasil perlu dirujuk ke parameter sebagaimana pada tambah.

Sejak C++14 tersedia juga decltype(auto), bentuk tipe kembalian yang meniru perilaku decltype, yaitu mempertahankan referensi dan kualifikasi const. Bentuk ini berguna pada fungsi yang mengembalikan akses ke elemen container, karena auto biasa menyalin nilainya, sedangkan decltype(auto) mengembalikan referensinya sebagaimana ekspresi return-nya. Untuk fungsi sederhana, auto biasa sudah cukup, dan decltype(auto) dipilih ketika kategori nilainya perlu dipertahankan.

Structured binding#

Structured binding membongkar objek majemuk, yaitu array, struct, atau pasangan nilai, menjadi beberapa nama dalam satu deklarasi. Pada perulangan stok, tiap elemen std::map adalah std::pair<const std::string, int>, dan bentuk const auto& [nama, banyak] memberi dua nama tersebut tanpa menulis it->first dan it->second yang sulit dibaca. Nama-namanya merujuk langsung ke elemen aslinya karena deklarasinya memakai const auto&, sehingga tidak terjadi penyalinan.

Hasil std::map::insert adalah std::pair<iterator, bool>, dan pembongkarannya dengan auto [posisi, berhasil] membuat makna setiap komponen terbaca dari namanya. Nama posisi dan berhasil menyatakan maksud setiap komponen, sesuatu yang tidak dapat disampaikan oleh first dan second. Struct buatan sendiri memperoleh manfaat yang sama, sehingga fungsi yang mengembalikan hasil berlabel ringan dapat ditulis dengan struct dan dibaca dengan satu baris deklarasi.

Struct buatan sendiri dan array dari struct memperoleh perlakuan yang sama, sebagaimana program berikut.

cpp evaluasi.cpp
#include <iostream>
#include <string>

struct HasilUjian {
    std::string nama;
    double nilai;
    bool lulus;
};

HasilUjian evaluasi(const std::string& nama, double nilai) {
    return {nama, nilai, nilai >= 75.0};
}

int main() {
    std::cout << std::boolalpha;
    HasilUjian hasil[] = {
        evaluasi("Ali", 82.5),
        evaluasi("Budi", 64.0),
        evaluasi("Citra", 90.0),
    };

    for (const auto& [nama, nilai, lulus] : hasil) {
        std::cout << nama << ": " << nilai << ", lulus: " << lulus << '\n';
    }

    // Tanpa tanda &: salinan dibuat, elemen aslinya tidak tersentuh.
    auto [nama1, nilai1, status1] = hasil[0];
    nilai1 = 0;
    std::cout << "elemen tetap: " << hasil[0].nilai << '\n';
    std::cout << "salinan diubah: " << nilai1 << '\n';
    return 0;
}
text Keluaran
Ali: 82.5, lulus: true
Budi: 64, lulus: false
Citra: 90, lulus: true
elemen tetap: 82.5
salinan diubah: 0

Perulangan di atas menggunakan const auto& agar setiap elemen dibaca tanpa penyalinan, sedangkan deklarasi auto [nama1, nilai1, status1] tanpa tanda & membuat salinan. Perbedaannya terbukti dari hasil[0].nilai yang tetap 82.5 setelah nilai1 diubah menjadi nol. Aturan ini sejalan dengan perilaku auto biasa, sehingga keputusan antara membaca dan menyalin tetap berada pada tanda &. Perlu dicatat juga bahwa structured binding tidak dapat dipakai pada daftar parameter fungsi, sehingga pembongkarannya tetap dilakukan di dalam badan fungsi.

Ringkasan#

  • auto menentukan tipe dari penginisialisasi dan melepaskan referensi serta const tingkat atas, sehingga bentuk auto& perlu dipilih secara sadar.
  • decltype mengambil tipe deklarasi atau ekspresi tanpa mengevaluasinya, dan mempertahankan referensi serta const.
  • Trailing return type memindahkan tipe hasil ke belakang panah agar dapat merujuk parameter template.
  • Structured binding membongkar pasangan nilai, struct, dan array menjadi nama-nama yang bermakna dalam satu deklarasi.

Referensi#