C++ tidak mengizinkan kita menulis “referensi ke referensi” secara langsung — int& & r = a; ditolak compiler. Tetapi di dalam template, alias, dan auto, komposisi seperti itu bisa terbentuk tanpa kita sadari: ketika T diduksi menjadi Pesan& dan parameternya T&&, compiler harus memutuskan apa arti Pesan& &&. Keputusannya adalah aturan reference collapsing: dua & yang bertemu runtuh menjadi satu, dan hanya kombinasi && + && yang bertahan sebagai rvalue reference. Aturan empat baris ini adalah mesin yang menjalankan forwarding reference dan std::forward — chapter ini membedahnya dengan bukti static_assert yang diperiksa penuh saat kompilasi.

Empat Kombinasi Collapsing #

Cukup satu prinsip: lvalue reference itu menular. Begitu salah satu sisi komposisi adalah & (lvalue reference), hasilnya &. Hanya && di kedua sisi yang menghasilkan &&:

KombinasiHasilKeterangan
& + &&lvalue reference bertemu lvalue reference
& + &&&lvalue reference menular
&& + &&lvalue reference menular
&& + &&&&satu-satunya jalan menghasilkan rvalue reference

Program berikut membangun keempat kombinasi lewat alias using, dan setiap klaim dijaga static_assert — bila satu pun aturan kita salah paham, file ini tidak bisa dikompilasi. Bagian kedua program menyimulasikan inti std::forward: static_cast<T&&>(x) yang kategorinya ditentukan oleh collapse:

cpp
// reference-collapsing.cpp — aturan &&  & -> & dan &&  && -> &&
// Kompilasi: g++ -std=c++23 -Wall -Wextra reference-collapsing.cpp -o reference-collapsing

#include <print>
#include <type_traits>
#include <utility>

using RefL = int&;    // alias lvalue reference
using RefR = int&&;   // alias rvalue reference

// Empat kombinasi collapsing — setiap baris dijaga static_assert:
using K1 = RefL&;     // int&  &  -> int&
using K2 = RefL&&;    // int&  && -> int&
using K3 = RefR&;     // int&& &  -> int&
using K4 = RefR&&;    // int&& && -> int&&

static_assert(std::is_same_v<K1, int&>);
static_assert(std::is_same_v<K2, int&>);
static_assert(std::is_same_v<K3, int&>);
static_assert(std::is_same_v<K4, int&&>);

// Alias template ikut mengalami collapsing:
template <typename T>
using AliasRef = T&;

static_assert(std::is_same_v<AliasRef<int&&>, int&>);   // int&& &  -> int&
static_assert(std::is_same_v<AliasRef<int&>, int&>);    // int&  &  -> int&

// Mini-std::forward: static_cast<T&&> memulihkan kategori lewat collapsing
template <typename T>
T&& teruskan(T&& x) {
    return static_cast<T&&>(x);
}

void cek(int&) {
    std::println("  hasil: lvalue (terikat ke int&)");
}
void cek(int&&) {
    std::println("  hasil: rvalue (terikat ke int&&)");
}

int main() {
    std::println("-- tabel collapsing (semua dijaga static_assert di atas)");
    std::println("  int&  &  -> int&");
    std::println("  int&  && -> int&");
    std::println("  int&& &  -> int&");
    std::println("  int&& && -> int&&");

    std::println("-- static_cast<T&&> memulihkan kategori (logika std::forward)");
    int a = 10;
    cek(teruskan<int&>(a));   // T = int&  : int&  &&  -> int&  (collapse)
    cek(teruskan<int>(20));   // T = int   : int&&           (tetap rvalue)
    cek(a);                   // langsung: lvalue
    cek(30);                  // langsung: prvalue
    return 0;
}

Output yang Dihasilkan:

text
-- tabel collapsing (semua dijaga static_assert di atas)
  int&  &  -> int&
  int&  && -> int&
  int&& &  -> int&
  int&& && -> int&&
-- static_cast<T&&> memulihkan kategori (logika std::forward)
  hasil: lvalue (terikat ke int&)
  hasil: rvalue (terikat ke int&&)
  hasil: lvalue (terikat ke int&)
  hasil: rvalue (terikat ke int&&)

Mari kita bedah:

  1. Baris 12-20 (empat kombinasi): K1 sampai K4 menyusun referensi-ke-referensi lewat alias. Empat static_assert menegaskan hasilnya: tiga kombinasi pertama runtuh ke int&, hanya int&& && yang bertahan sebagai int&&.
  2. Baris 23-27 (alias template): AliasRef<T> didefinisikan sebagai T&. Ketika T diisi int&&, hasilnya int&& & yang runtuh menjadi int& — collapsing terjadi pada setiap lapis komposisi, bukan hanya yang pertama.
  3. Baris 31-33 (mini std::forward): teruskan(T&& x) mengembalikan static_cast<T&&>(x). Bentuk ini persis logika std::forward<T>: kembalikan argumen dengan kategori yang dihitung dari T.
  4. Baris 51 (teruskan<int&>(a)): T = int& membuat tipe kembalian int& && yang runtuh menjadi int& — parameter di baris 31 yang berupa lvalue di-cast ke int&, sehingga cek(int&) yang terpilih: kategori lvalue dipertahankan.
  5. Baris 52 (teruskan<int>(20)): T = int membuat tipe kembalian int&& murni; cast static_cast<int&&> mengubah parameter bernama (yang lazimnya lvalue) menjadi rvalue, sehingga cek(int&&) yang terpilih.

Baris 51 dan 52 adalah penjelasan mekanis untuk chapter Perfect Forwarding: std::forward<T>(a) “tahu” kategori asal karena T menyimpannya (Pesan& untuk lvalue, Pesan untuk rvalue), dan static_cast<T&&> menerjemahkan kategori itu kembali lewat collapse. Tanpa collapsing, satu template tidak mungkin menghasilkan dua tipe referensi berbeda.

Mengapa Tidak Boleh Ditulis Langsung #

Menulis int& & di source code memang ditolak, dan keputusan itu masuk akal: referensi bukan objek — ia hanya nama lain untuk lokasi yang sudah ada — sehingga “referensi ke referensi” tidak punya arti fisik di memori. Collapsing bukan operasi yang kita panggil; ia adalah cara compiler menyederhanakan tipe yang terbentuk di konteks-konteks template, alias, decltype, dan auto. Semua jalur itu berujung pada dua nama saja: lvalue reference atau rvalue reference.

Dengan collapsing, cerita forwarding lengkap: deduksi T&& menyimpan kategori, alias std::forward<T> memulihkannya lewat static_cast<T&&>, dan collapse memastikan hasilnya referensi yang benar. Kini kita beralih ke sisi optimasi: bagaimana compiler bisa melewati copy dan move sama sekali lewat copy elision.

Rujukan resmi: cppreference — reference declaration , cppreference — std::forward .