Ketika kita menulis auto x = ..., kita sebenarnya sedang meminta compiler melakukan pekerjaan yang dulu kita lakukan manual: membaca tipe inisialisasi, menerapkan aturan deduksi, lalu memutuskan tipe akhir variabel. Pekerjaan ini punya aturan yang tegas dan konsisten — bukan tebakan — dan memahami aturannya adalah pembeda antara auto yang membuat kode lebih aman atau lebih licik. Chapter Uniform Initialization ditutup dengan pertanyaan “tipe apa yang compiler lihat dari inisialisasi itu?”; chapter ini menjawabnya secara sistematis: auto untuk variabel, kembalian fungsi (C++14), dan parameter (C++20), lalu decltype yang membaca tipe tanpa mengevaluasi ekspresi, decltype(auto) yang menjaga referensi, dan structured bindings yang menyulap satu deklarasi auto menjadi beberapa nama sekaligus. Semua aturan kita buktikan lewat static_assert — pemeriksaan tipe yang dijalankan compiler saat kompilasi, sehingga klaim di chapter ini bukan opini melainkan fakta yang diverifikasi.

Program Pengujian Aturan Deduksi #

cpp
// type-deduction.cpp — aturan deduksi auto/decltype yang dipraktikkan
// Kompilasi: g++ -std=c++23 -Wall -Wextra type-deduction.cpp -o type-deduction

#include <iostream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>

int g = 0;
int& ambilRef() { return g; }          // mengembalikan referensi ke global

// return type deduction (C++14): tipe kembalian disimpulkan
constexpr auto kuadrat(int x) { return x * x; }

// decltype(auto): MEMPERTAHANKAN tipe penuh, termasuk referensinya
decltype(auto) lewatRef() { return ambilRef(); }

// auto pada parameter (C++20) = singkatan template
auto dobel(auto x) { return x * 2; }

int main() {
    int sumber = 10;
    const int& ref = sumber;

    // Aturan inti: auto by value MELEPAS const dan reference -> salinan
    auto a = ref;                      // a: int (salinan)
    auto& b = sumber;                  // b: int&
    const auto& c = ref;               // c: const int&
    static_assert(std::is_same_v<decltype(a), int>);
    static_assert(std::is_same_v<decltype(b), int&>);
    static_assert(std::is_same_v<decltype(c), const int&>);

    a = 99;                            // mengubah SALINAN
    std::cout << "auto  = ref -> sumber tetap: " << sumber
              << ", a jadi: " << a << "\n";
    b = 7;                             // mengubah sumber lewat referensi
    std::cout << "auto&       -> sumber ikut  : " << sumber << "\n";

    // Konsekuensinya di range-for
    std::vector<int> v{1, 2, 3};
    for (auto x : v) {                 // salinan tiap elemen
        x *= 10;                       // mengubah salinan: v tetap
    }
    std::cout << "loop by value: " << v[0] << v[1] << v[2] << "\n";
    for (auto& x : v) {                // referensi ke elemen asli
        x *= 10;                       // mengubah v sungguhan
    }
    std::cout << "loop auto&   : " << v[0] << v[1] << v[2] << "\n";

    // decltype: tipe dari ekspresi, tanpa mengevaluasi ekspresinya
    decltype(v.size()) n = v.size();   // size_type, bukan int
    static_assert(std::is_same_v<decltype(n), std::size_t>);
    std::cout << "decltype     : n=" << n << "\n";

    int& r = ambilRef();
    decltype(r) r2 = g;                // decltype mempertahankan int&
    r2 = 1;
    std::cout << "decltype(int&)   -> g=" << g << "\n";

    // decltype(auto) pada return: referensi tidak dilepas
    lewatRef() = 42;
    std::cout << "decltype(auto)   -> g=" << g << "\n";

    static_assert(kuadrat(5) == 25);   // kuadrat constexpr: dihitung saat kompilasi
    std::cout << "auto return  : " << kuadrat(6) << ", dobel: "
              << dobel(21) << " " << dobel(2.5) << "\n";

    // structured bindings: keluarga auto untuk membongkar pasangan
    std::pair<int, std::string> pasangan{5, "MERAH"};
    auto& [nomor, label] = pasangan;   // binding sebagai referensi
    nomor = 6;
    label = "BIRU";
    std::cout << "binding auto&: " << pasangan.first
              << " " << pasangan.second << "\n";
    return 0;
}

Output yang Dihasilkan:

text
auto  = ref -> sumber tetap: 10, a jadi: 99
auto&       -> sumber ikut  : 7
loop by value: 123
loop auto&   : 102030
decltype     : n=3
decltype(int&)   -> g=1
decltype(auto)   -> g=42
auto return  : 36, dobel: 42 5
binding auto&: 6 BIRU

Mari kita bedah:

  1. Baris 27-32 (aturan inti): auto a = ref; menghasilkan int, bukan const int&. Saat menduksi by value, compiler melepas reference dan const puncak dari tipe sumber, lalu membuat salinan. Tiga static_assert di baris 30-32 membuktikan tipe hasilnya: int, int&, dan const int& — masing-masing persis seperti yang kita tulis.
  2. Baris 34-38 (akibat perilaku): mengisi a = 99 mengubah salinan, sehingga sumber tetap 10; mengisi b = 7 mengubah sumber sungguhan lewat referensi, sehingga tercetak 7. Ini alasan bug berbasis auto sering “diam”: keduanya kompilasi normal, bedanya hanya siapa yang berubah.
  3. Baris 42-49 (konsekuensi di loop): for (auto x : v) menyalin tiap elemen sehingga v tetap 1 2 3; for (auto& x : v) mengikat referensi ke elemen asli sehingga v berubah menjadi 10 20 30. Pola ini akan kita perdalam pada chapter range-based for.
  4. Baris 52-54 (decltype): decltype(v.size()) menghasilkan std::size_t — tipe kembalian size() yang panjang dan mudah salah diketik — tanpa kita perlu tahu menulisnya. static_assert mengonfirmasi bahwa n benar-benar std::size_t, bukan int yang kebetulan cocok.
  5. Baris 56-59 (decltype menjaga referensi): karena r bertipe int&, decltype(r) menghasilkan int& — referensinya dipertahankan. Mengisi r2 = 1 mengubah g yang dirujuk, terbukti dari keluaran g=1.
  6. Baris 62-63 (decltype(auto)): lewatRef() = 42; legal dan mengubah g menjadi 42 — artinya kembalian fungsi ini adalah referensi hidup, bukan salinan. Bandingkan bila kembaliannya ditulis auto: salinan int dikembalikan dan penugasan seperti itu tidak bermakna.
  7. Baris 65-67 (auto pada return dan parameter): kuadrat menyimpulkan kembaliannya int (C++14), bahkan bisa constexpr; dobel memakai parameter auto (C++20) sehingga satu fungsi melayani dobel(21) dan dobel(2.5) — di belakang layar compiler membangun template dengan dua instansiasi.

Aturan Ringkas Deduksi auto #

Aturan di atas bisa diringkas ke dalam satu kalimat: by value mengupas, by reference mengawetkan. Tabel berikut memetakan bentuk deklarasi ke efeknya, dengan ref bertipe const int&:

Bentuk deklarasiTipe hasilEfek
auto x = ref;intsalinan; const dan & dilepas
auto& x = ref;const int&alias; const sumber dipertahankan
const auto& x = ref;const int&alias baca-saja, aman untuk data besar
auto&& x = ref;mengikuti sisi kananforwarding reference — lvalue menjadi &, rvalue menjadi &&

Baris terakhir memperkenalkan istilah yang akan penting di chapter Perfect Forwarding: auto&& bukan “referensi rvalue”, melainkan forwarding reference yang menyesuaikan diri dengan sisi kanannya — lvalue terikat sebagai int&, sementara sementara (temporary) terikat sebagai int&&. Untuk variabel biasa sehari-hari, kita cukup mengingat dua pilihan utama: salin dengan auto bila memang ingin kepemilikan sendiri, dan ambil const auto& bila hanya membaca data yang berat seperti std::string atau std::vector.

Ada satu variasi kurung kurawal yang jarang disadari:

cpp auto dengan kurung kurawal: dua tipe berbeda
auto a{5};       // int — satu nilai tunggal di dalam {}
auto b = {5};    // std::initializer_list<int> — tanda = mengubah makna
auto c{5, 10};   // error sejak C++17: kurung kurawal multi-elemen ditolak

Tanda = di baris kedua membuat compiler membaca {5} sebagai daftar, sehingga tipe yang diduksi adalah std::initializer_list<int> — bukan int. Karena std::initializer_list merujuk data di belakang layar, menyimpannya ke variabel yang hidup lebih lama dari daftarnya adalah jebakan; deklarasi auto a{5}; bebas dari masalah itu.

decltype: Tipe dari Ekspresi #

decltype dan auto sering disandingkan padahal keduanya menjawab pertanyaan berbeda. auto berarti “buat variabel ini dengan tipe yang cocok untuk inisialisasi ini”, sedangkan decltype berarti “sebutkan tipe dari ekspresi ini” — tanpa mengeksekusinya, tanpa membuat variabel. Karena tidak mengevaluasi, decltype boleh menyebut ekspresi yang tidak sah saat runtime, misalnya decltype(f()) x; mendeklarasikan x dengan tipe kembalian f() tanpa benar-benar memanggil f().

Dua kegunaan praktisnya terlihat pada program di atas. Pertama, tipe yang panjang seperti kembalian v.size() tidak perlu diketik ulang (baris 52) — kita tetap mendapat tipe yang presisi, bukan int yang berpotensi terpotong di platform berbeda. Kedua, decltype menjaga referensi: decltype(r) menghasilkan int& sedangkan auto x = r; hanya menghasilkan int. Saat menulis kode generik dengan template, perbedaan ini menentukan apakah kita meneruskan referensi atau memotongnya diam-diam.

decltype(auto): auto yang Menjaga Referensi #

Fungsi lewatRef() pada baris 17 mengembalikan decltype(auto), bukan auto. Jika kita menulis auto lewatRef(), compiler menerapkan aturan by value: tipe kembalian menjadi int, referensi dilepas, dan pemanggil menerima salinan — penugasan lewatRef() = 42; bahkan tidak akan dikompilasi. Dengan decltype(auto), compiler menyimpulkan tipe persis seperti decltype pada ekspresi kembaliannya, sehingga int& tetap int& dan penugasan lewat kembalian mengubah objek aslinya.

Kapan kita memakainya? Terutama di kode template atau wrapper yang meneruskan kembalian fungsi lain — kita belum tahu apakah fungsi yang dibungkus mengembalikan nilai atau referensi, dan decltype(auto) meneruskan apa adanya tanpa memaksa salinan. Untuk fungsi biasa dengan tipe yang sudah jelas, menulis tipe kembalian secara eksplisit tetap lebih mudah dibaca. Anggap decltype(auto) sebagai alat presisi di kotak peralatan: jarang terpakai, tetapi saat dibutuhkan tidak ada penggantinya.

Structured Bindings: Anggota Keluarga auto #

Baris 71 menunjukkan bentuk deklarasi yang masih satu keluarga: auto& [nomor, label] = pasangan;. Di sini compiler menduksi tipe setiap elemen std::pair lalu mengikatnya ke dua nama — nomor dan label bukan variabel baru, melainkan nama alternatif untuk pasangan.first dan pasangan.second. Modifier &, const auto&, atau auto polos berlaku dengan aturan yang sama seperti tabel di atas: by value menyalin, by reference mengalias.

Mengubah nomor = 6 dan label = "BIRU" langsung mengubah pasangan — keluaran 6 BIRU membuktikannya. Fitur ini lahir di C++17 dan menjadi gaya bawaan ketika mengiterasi std::map atau menerima beberapa kembalian sekaligus; karena cukup penting, kita bedah tuntas pada chapter Structured Bindings yang membahasnya untuk struct, std::pair, std::tuple, dan std::array.

Dengan aturan deduksi ini, auto berhenti terasa seperti tebak-tebakan dan mulai terasa seperti kontrak yang bisa kita baca. Bab berikutnya mengajak deduksi ini bekerja di tempat yang paling sering kita tulis sehari-hari: loop range-based for, lengkap dengan perangkap umur temporary dan cara membuat class sendiri ikut didukung.

Rujukan resmi: cppreference — auto , cppreference — decltype , cppreference — structured binding declaration .