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 &&:
| Kombinasi | Hasil | Keterangan |
|---|---|---|
& + & | & | 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:
// 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:
-- 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:
- Baris 12-20 (empat kombinasi):
K1sampaiK4menyusun referensi-ke-referensi lewat alias. Empatstatic_assertmenegaskan hasilnya: tiga kombinasi pertama runtuh keint&, hanyaint&& &&yang bertahan sebagaiint&&. - Baris 23-27 (alias template):
AliasRef<T>didefinisikan sebagaiT&. KetikaTdiisiint&&, hasilnyaint&& &yang runtuh menjadiint&— collapsing terjadi pada setiap lapis komposisi, bukan hanya yang pertama. - Baris 31-33 (mini std::forward):
teruskan(T&& x)mengembalikanstatic_cast<T&&>(x). Bentuk ini persis logikastd::forward<T>: kembalikan argumen dengan kategori yang dihitung dariT. - Baris 51 (
teruskan<int&>(a)):T = int&membuat tipe kembalianint& &&yang runtuh menjadiint&— parameter di baris 31 yang berupa lvalue di-cast keint&, sehinggacek(int&)yang terpilih: kategori lvalue dipertahankan. - Baris 52 (
teruskan<int>(20)):T = intmembuat tipe kembalianint&&murni; caststatic_cast<int&&>mengubah parameter bernama (yang lazimnya lvalue) menjadi rvalue, sehinggacek(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 .