Chapter Perfect Forwarding di Section Modern C++ sudah memperlihatkan pola ini bekerja pada wrapper nyata seperti emplace_back. Sekarang, dengan bekal value category, rvalue reference, dan std::move yang sudah kita kuasai, kita bisa membongkar mekanismenya sampai ke akarnya. Pertanyaan intinya: sebuah fungsi perantara generik menerima argumen dari pemanggil, lalu meneruskannya ke fungsi lain — bagaimana caranya agar lvalue tetap diperlakukan sebagai lvalue dan rvalue tetap dipindahkan, tanpa menambah salinan satu pun? Jawabannya dua bagian yang berpasangan: parameter T&& yang diduksi khusus (forwarding reference) dan std::forward<T> yang memulihkan kategori asal argumen.

Masalahnya: Overload Berlipat Ganda #

Coba bayangkan menyelesaikan masalah ini tanpa template. Untuk satu parameter bertipe Pesan, kategori dan const-ness pemanggil bisa empat bentuk: Pesan&, const Pesan&, Pesan&&, dan const Pesan&&. Agar setiap pemanggil diperlakukan optimal, kita perlu menulis empat overload — dan untuk dua parameter, 16; untuk tiga, 64; jumlahnya tumbuh 4^N. Tidak ada tim yang sanggup merawat kombinasi seperti itu, dan std::move saja tidak menolong: move selalu menghasilkan rvalue, sehingga wrapper yang meneruskan dengan std::move akan memindahkan lvalue milik pemanggil secara diam-diam. Yang dibutuhkan adalah satu parameter tunggal yang menyimpan kategori asal argumen.

Deduksi T&& Menyimpan Kategori Asal #

Parameter T&& pada template punya aturan deduksi unik. Ketika pemanggil mengirim lvalue bertipe Pesan, compiler menduksi T menjadi Pesan&, sehingga parameter T&& runtuh menjadi Pesan& (lvalue reference). Ketika pemanggil mengirim rvalue, T diduksi polos sebagai Pesan, dan parameter menjadi Pesan&&. Informasi kategori asal tersimpan rapi di T — tetapi ada jebakan: begitu kita menyebut nama parameternya di dalam badan fungsi, parameter itu kembali menjadi lvalue, karena setiap ekspresi bernama adalah lvalue. Inilah yang diperbaiki std::forward<T>: ia membaca T dan mengembalikan argumen dengan kualitas asalnya.

Program berikut menguji kedua jalur pemanggilan sekaligus, dengan dua overload penerima sebagai indikator kategori yang sampai di tujuan:

cpp
// perfect-forwarding.cpp — uji deduksi lvalue vs rvalue pada T&& dan std::forward
// Kompilasi: g++ -std=c++23 -Wall -Wextra perfect-forwarding.cpp -o perfect-forwarding

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

class Pesan {
public:
    explicit Pesan(std::string teks) : teks_(std::move(teks)) {}
    const std::string& teks() const { return teks_; }
private:
    std::string teks_;
};

// Dua penerima: pilihan mereka menunjukkan kategori yang sampai di tujuan
void simpan(Pesan& p) {
    std::println("  -> simpan(Pesan&)  : sampai sebagai lvalue '{}'", p.teks());
}
void simpan(Pesan&& p) {
    std::println("  -> simpan(Pesan&&) : sampai sebagai rvalue '{}'", p.teks());
}

// Wrapper generik: T&& di sini adalah forwarding reference
template <typename T>
void salurkan(T&& argumen) {
    std::println("  T diduksi sebagai referensi-lvalue? {}",
                 std::is_lvalue_reference_v<T>);
    simpan(argumen);                   // parameter bernama = selalu lvalue
    simpan(std::forward<T>(argumen));  // kualitas asal argumen dipulihkan
}

int main() {
    Pesan tetap("objek lama");

    std::println("-- salurkan(tetap): pemanggil mengirim lvalue");
    salurkan(tetap);

    std::println("-- salurkan(temporary Pesan): pemanggil mengirim rvalue");
    salurkan(Pesan("sementara"));
    return 0;
}

Output yang Dihasilkan:

text
-- salurkan(tetap): pemanggil mengirim lvalue
  T diduksi sebagai referensi-lvalue? true
  -> simpan(Pesan&)  : sampai sebagai lvalue 'objek lama'
  -> simpan(Pesan&)  : sampai sebagai lvalue 'objek lama'
-- salurkan(temporary Pesan): pemanggil mengirim rvalue
  T diduksi sebagai referensi-lvalue? false
  -> simpan(Pesan&)  : sampai sebagai lvalue 'sementara'
  -> simpan(Pesan&&) : sampai sebagai rvalue 'sementara'

Mari kita bedah:

  1. Baris 27-32 (wrapper generik): salurkan(T&& argumen) adalah forwarding reference. Baris 28-29 mencetak hasil deduksi T secara langsung lewat std::is_lvalue_reference_v<T>.
  2. Baris 30 vs 31 (dua penerusan): simpan(argumen) menyebut parameter bernama — hasilnya selalu lvalue, apa pun asalnya. simpan(std::forward<T>(argumen)) memulihkan kategori asal sebelum meneruskan.
  3. Pemanggilan pertama (lvalue): T diduksi Pesan& (cetak true), dan kedua penerusan sampai sebagai lvalue — std::forward jujur: pemanggil mengirim objek yang masih dipakai, jadi objek itu tidak boleh dipindahkan.
  4. Pemanggilan kedua (rvalue): T diduksi Pesan (cetak false). Penerusan tanpa forward jatuh ke simpan(Pesan&) — kualitas rvalue hilang di parameter bernama — sedangkan penerusan dengan std::forward sampai ke simpan(Pesan&&): jalur pindahan tetap terbuka, tanpa salinan ekstra.

Dua baris pemanggilan simpan(...) di dalam wrapper adalah eksperimen terkontrol: satu-satunya perbedaan adalah kehadiran std::forward, dan hasilnya persis menunjukkan apa yang rusak bila forward dilupakan. Pola 4^N yang mustahil ditulis manual kini diselesaikan satu template tunggal.

Dua Aturan yang Tidak Boleh Dilanggar #

Pertama, std::forward wajib ditulis dengan argumen template-nya: std::forward<T>(argumen), bukan std::forward(argumen). Tanpa T, fungsi itu tidak tahu kategori asal yang harus dipulihkan, karena informasi itu tersimpan persis di T hasil deduksi. Kedua, T&& dan std::forward<T> adalah pasangan yang tidak boleh berpisah — parameter forwarding tanpa forward menghapus rvalue-ness (jadi copy diam-diam), sedangkan std::forward tanpa parameter forwarding tidak pernah muncul sebagai pola yang benar. Kontrasnya dengan std::move tegas: move selalu menghasilkan rvalue (pakai saat kita sendiri menyerahkan objek), forward mengikuti asal argumen (pakai saat kita hanya perantara).

Satu istilah yang masih perlu kita presisikan: kapan persisnya T&& menjadi forwarding reference, dan kapan ia hanya rvalue reference biasa? Jawabannya ada pada satu kata — deduksi — dan itulah topik penuh chapter berikutnya.

Rujukan resmi: cppreference — std::forward , cppreference — overload resolution .