Notasi T&& di chapter-chapter sebelumnya memakai dua peran yang tampak sama tetapi berbeda total. Pada move constructor, Buffer(Buffer&& lain) adalah rvalue reference murni: parameter itu hanya mau menerima rvalue. Pada template salurkan(T&& argumen), notasi yang sama adalah forwarding reference: parameter itu menerima lvalue maupun rvalue, dan kategori asalnya tersimpan di deduksi T. Pembedanya satu kata: apakah T sedang diduksi dari argumen pemanggil. Chapter ini menetapkan aturannya, menguji auto&& yang mengikuti mekanisme yang sama, dan memperlihatkan apa yang terjadi bila kita mengira semua T&& adalah forwarding reference.

Aturan: Konteks Deduksi Menentukan Segalanya #

Aturan dari standar: T&& adalah forwarding reference jika dan hanya jika T adalah parameter template dari fungsi yang sedang diduksi dari argumen pemanggil. Konteks yang termasuk aturan ini:

  • template <typename T> void f(T&& x) — T diduksi dari pemanggil → forwarding reference.
  • auto&& x = ... — auto memainkan peran T dengan aturan yang sama → forwarding reference.
  • template <typename... Args> void f(Args&&... args) — tiap anggota paket ikut diduksi → forwarding reference (bentuk yang dipakai emplace_back dan std::make_unique).

Konteks yang bukan forwarding reference:

  • void f(Pesan&& x) — tipe konkret, tidak ada deduksi → rvalue reference murni.
  • void f(const T&& x) — ada const di depan → rvalue reference murni.
  • template <typename T> class Wadah { void ganti(T&& baru); } — T ditentukan saat class diinstansiasi, bukan diduksi dari argumen pemanggil ganti() → rvalue reference murni.

Kasus terakhir paling sering mengejutkan. Meskipun Wadah adalah template, parameter fungsi ganti(T&&) tidak berpartisipasi dalam deduksi apa pun: pada Wadah<std::string> w;, tipe T sudah terkunci sebagai std::string jauh sebelum pemanggil memilih argumen untuk ganti().

Program: Menguji Ketiga Konteks #

Program berikut menjalankan ketiga situasi sekaligus — template fungsi, auto&&, dan member class — dengan static_assert sebagai penjaga tipe:

cpp
// forwarding-reference.cpp — T&& yang diduksi vs T&& yang sudah pasti
// Kompilasi: g++ -std=c++23 -Wall -Wextra forwarding-reference.cpp -o forwarding-reference

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

// Kasus 1: T&& pada template yang parameternya DIDUKSI = forwarding reference
template <typename T>
void deduksi(T&&) {
    if constexpr (std::is_lvalue_reference_v<T>) {
        std::println("  T diduksi jadi T&  -> parameter = lvalue reference");
    } else {
        std::println("  T diduksi jadi T   -> parameter = rvalue reference");
    }
}

// Kasus 2: T&& pada class yang T-nya SUDAH DITENTUKAN = rvalue reference murni
template <typename T>
class Wadah {
public:
    explicit Wadah(T nilai) : nilai_(std::move(nilai)) {}
    void ganti(T&& baru) {              // bukan forwarding reference
        nilai_ = std::move(baru);
        std::println("  Wadah<T>::ganti(T&&) menerima rvalue: '{}'", nilai_);
    }
private:
    T nilai_;
};

std::string buatString() { return "dari fungsi"; }

int main() {
    std::string s = "teks";

    std::println("-- template: T&& adalah forwarding reference (T ikut diduksi)");
    deduksi(s);                          // lvalue
    deduksi(std::string("sementara"));   // rvalue
    deduksi(buatString());               // rvalue (prvalue)

    std::println("-- auto&&: aturan deduksi yang sama dengan T&&");
    auto&& r1 = s;                       // lvalue -> r1 = std::string&
    auto&& r2 = buatString();            // rvalue -> r2 = std::string&&
    static_assert(std::is_lvalue_reference_v<decltype(r1)>);
    static_assert(std::is_rvalue_reference_v<decltype(r2)>);
    std::println("  r1 referensi-lvalue? {} (isi '{}')",
                 std::is_lvalue_reference_v<decltype(r1)>, r1);
    std::println("  r2 referensi-lvalue? {} (isi '{}')",
                 std::is_lvalue_reference_v<decltype(r2)>, r2);

    std::println("-- class member: T&& = rvalue reference murni (T tidak diduksi)");
    Wadah<std::string> w("isi awal");
    w.ganti(std::string("isi baru"));    // hanya rvalue yang diterima
    return 0;
}

Output yang Dihasilkan:

text
-- template: T&& adalah forwarding reference (T ikut diduksi)
  T diduksi jadi T&  -> parameter = lvalue reference
  T diduksi jadi T   -> parameter = rvalue reference
  T diduksi jadi T   -> parameter = rvalue reference
-- auto&&: aturan deduksi yang sama dengan T&&
  r1 referensi-lvalue? true (isi 'teks')
  r2 referensi-lvalue? false (isi 'dari fungsi')
-- class member: T&& = rvalue reference murni (T tidak diduksi)
  Wadah<T>::ganti(T&&) menerima rvalue: 'isi baru'

Mari kita bedah:

  1. Baris 11-17 (deduksi T&&): deduksi(T&&) melayani tiga pemanggilan. Untuk s (lvalue), T diduksi std::string& sehingga cabang pertama berlaku; untuk dua rvalue, T diduksi std::string dan cabang kedua yang berlaku. Satu template, dua perilaku, tanpa overload.
  2. Baris 43-44 (auto&&): r1 mengikat lvalue → auto diduksi std::string&; r2 mengikat prvalue → auto diduksi std::string. static_assert di baris 45-46 membuktikan kategori referensi keduanya pada waktu kompilasi.
  3. Baris 24 (member class): ganti(T&& baru) menerima hanya rvalue — sesuai aturan, ini rvalue reference murni, bukan forwarding reference. Pemanggilan di baris 54 mengirim std::string("isi baru") dan lolos.
  4. Baris 12-16 (if constexpr): pemilihan cabang terjadi saat kompilasi berdasarkan T hasil deduksi — cara paling langsung membaca apa yang compiler putuskan.

Sekarang bagian yang tidak lolos: apa yang terjadi bila kita mengirim lvalue ke ganti()? Kita coba di file terpisah:

cpp mengirim lvalue ke member T&& yang bukan forwarding reference (tidak dikompilasi)
Wadah<std::string> w("isi awal");
w.ganti(s);   // s adalah lvalue

Pesan error yang dihasilkan compiler:

text
ch8-lvalue.cpp: In function ‘int main()’:
ch8-lvalue.cpp:15:13: error: cannot bind rvalue reference of type ‘std::__cxx11::basic_string<char>&&’ to lvalue of type ‘std::string’ {aka ‘std::__cxx11::basic_string<char>’}
   15 |     w.ganti(s);   // gagal: lvalue tidak boleh terikat ke rvalue reference murni
      |             ^
ch8-lvalue.cpp:8:20: note:   initializing argument 1 of ‘void Wadah<T>::ganti(T&&) [with T = std::__cxx11::basic_string<char>]’
    8 |     void ganti(T&& baru) { nilai_ = std::move(baru); }
      |                ~~~~^~~~

Pesan cannot bind rvalue reference ... to lvalue adalah konsekuensi logis: karena T&& di sini bukan forwarding reference, tidak ada deduksi yang bisa mengubahnya menjadi lvalue reference. Solusinya dua: kirim std::move(s) bila memang ingin memindahkan, atau sediakan overload ganti(const T&) bila isinya perlu dipertahankan.

Universal Reference: Nama Lama untuk Konsep yang Sama #

Di literatur — terutama buku dan artikel Scott Meyers — istilah yang dulu dipakai adalah universal reference. Standar C++ kemudian menetapkan istilah resmi forwarding reference, dan istilah inilah yang dipakai pesan error serta dokumentasi modern. Keduanya menunjuk mekanisme yang sama: parameter yang diduksi khusus sehingga bisa mengikat kedua kategori. Bila membaca materi lama, perlakukan kedua istilah itu sebagai sinonim.

Di balik deduksi yang menyimpan kategori ini ada mekanisme aritmetika tipe yang membuat Pesan& && bisa ada: reference collapsing. Itulah mesin yang juga menjalankan std::forward, dan kita bedah penuh di chapter berikutnya.

Rujukan resmi: cppreference — template argument deduction , cppreference — reference declaration .