Chapter Move Semantics berakhir dengan satu pertanyaan terbuka: bagaimana fungsi generik meneruskan argumen ke fungsi lain tanpa merusak kualitas pindahannya? Masalahnya nyata di setiap wrapper. std::make_unique<T>(...), emplace_back(...), dan constructor std::thread semuanya harus meneruskan argumen milik pemanggil ke constructor dalam — bila pemanggil mengirim objek berat sementara, penerusan tidak boleh menambahkan salinan; bila pemanggil mengirim lvalue yang masih dipakai, penerusan tidak boleh ikut memindahkannya. Solusinya dua bagian: parameter T&& yang diduksi secara khusus (forwarding reference) dan std::forward<T> yang mengembalikan kualitas asal argumen. Keduanya kita buktikan dengan penghitung copy/move, supaya bukan sekadar klaim performa.

Masalah: Parameter Bernama Melupakan Asalnya #

Ingat mekanisme move: constructor move hanya dipilih ketika argumennya ekspresi rvalue. Sekarang perhatikan wrapper apa pun yang menerima parameter bernama — termasuk parameter T&& sekalipun. Di dalam badan fungsi, parameter bernama selalu lvalue: ia punya nama, bisa diambil alamatnya, dan compiler tidak tahu (juga tidak peduli) apakah yang diikat tadi temporary atau objek berumur panjang. Akibatnya, wrapper yang menulis Pesan(a...) selalu memilih copy constructor, meski pemanggil mengirim temporary yang sengaja dibuat untuk dipindahkan.

Parameter T&& pada template punya perilaku deduksi unik yang membuat pola ini mungkin. Untuk template parameter Args, parameter fungsi Args&& adalah forwarding reference: bila argumen adalah lvalue Pesan, Args diduksi menjadi Pesan& sehingga parameter menjadi Pesan& (reference ke lvalue); bila argumen adalah rvalue, Args diduksi menjadi Pesan dan parameter menjadi Pesan&&. Dengan kata lain, parameter menyimpan kualitas asal argumen — tetapi begitu kita menyebut nama parameternya di dalam badan, kualitas itu hilang lagi karena namanya lvalue. Misi std::forward adalah memulihkan informasi itu.

Program: Menghitung Salinan yang Hemat #

Class Pesan menghitung setiap copy dan move ke variabel statis, sehingga perilaku dua versi wrapper terlihat hitam di atas putih:

cpp
// forwarding.cpp — perfect forwarding: meneruskan argumen tanpa salinan ekstra
// Kompilasi: g++ -std=c++23 -Wall -Wextra forwarding.cpp -o forwarding

#include <print>
#include <string>
#include <utility>
#include <vector>

struct Pesan {
    static int salinan;
    static int pindahan;
    std::string isi;

    explicit Pesan(std::string t) : isi(std::move(t)) {}
    Pesan(const Pesan& lain) : isi(lain.isi) { ++salinan; }
    Pesan(Pesan&& lain) noexcept : isi(std::move(lain.isi)) { ++pindahan; }
};

int Pesan::salinan = 0;
int Pesan::pindahan = 0;

// TANPA std::forward: parameter bernama a... adalah lvalue
template <typename... Args>
Pesan buatSalah(Args&&... a) {
    return Pesan(a...);                    // temporary dari pemanggil -> tetap dicopy
}

// DENGAN std::forward: kualitas lvalue/rvalue dari sumber dipertahankan
template <typename... Args>
Pesan buatBenar(Args&&... a) {
    return Pesan(std::forward<Args>(a)...);
}

// pola emplace-style: argumen diteruskan ke constructor elemen
template <typename... Args>
void tambah(std::vector<std::string>& v, Args&&... a) {
    (v.emplace_back(std::forward<Args>(a)), ...);   // fold expression C++17
}

int main() {
    std::print("-- wrapper tanpa dan dengan std::forward\n");
    Pesan::salinan = Pesan::pindahan = 0;
    Pesan a = buatSalah(Pesan("dari temporary"));
    std::print("buatSalah: salinan={} pindahan={} isi={}\n",
               Pesan::salinan, Pesan::pindahan, a.isi);

    Pesan::salinan = Pesan::pindahan = 0;
    Pesan b = buatBenar(Pesan("dari temporary"));
    std::print("buatBenar: salinan={} pindahan={} isi={}\n",
               Pesan::salinan, Pesan::pindahan, b.isi);

    Pesan sumber("objek lama");
    Pesan::salinan = Pesan::pindahan = 0;
    Pesan c = buatBenar(sumber);           // lvalue: memang benar dicopy
    std::print("lvalue    : salinan={} pindahan={} isi={}\n",
               Pesan::salinan, Pesan::pindahan, c.isi);

    std::print("-- emplace-style: argumen diteruskan ke constructor elemen\n");
    std::vector<std::string> kata;
    tambah(kata, "alpha", "beta", "gamma");
    for (const auto& k : kata) std::print("{} ", k);
    std::print("\n");
    return 0;
}

Output yang Dihasilkan:

text
-- wrapper tanpa dan dengan std::forward
buatSalah: salinan=1 pindahan=0 isi=dari temporary
buatBenar: salinan=0 pindahan=1 isi=dari temporary
lvalue    : salinan=1 pindahan=0 isi=objek lama
-- emplace-style: argumen diteruskan ke constructor elemen
alpha beta gamma 

Mari kita bedah:

  1. Baris 23-26 (wrapper tanpa forward): buatSalah(Pesan("dari temporary")) menerima temporary, tetapi return Pesan(a...); menyebut parameter bernama a... — lvalue — sehingga copy constructor terpilih: salinan=1, pindahan=0. Buffer string "dari temporary" disalin penuh padahal sumbernya tinggal dibuang.
  2. Baris 29-32 (wrapper dengan forward): std::forward<Args>(a)... mengembalikan a... sebagai rvalue karena sumbernya rvalue, sehingga move constructor terpilih: salinan=0, pindahan=1. Satu-satunya biaya adalah transfer pointer — penerusan yang “sempurna” (perfect): apa pun kualitas asalnya, tidak ada salinan ekstra.
  3. Baris 52-56 (lvalue tetap lvalue): pemanggil buatBenar(sumber) mengirim lvalue yang masih dipakai; std::forward meneruskannya sebagai lvalue sehingga copy terjadi secara sah (salinan=1) — justru ini perilaku yang benar, karena sumber harus tetap utuh di main().
  4. Baris 35-38 (emplace-style): tambah() meneruskan tiap argumen ke emplace_back, yang membangun elemen std::string langsung di dalam container — tanpa objek perantara. Ekspresi (fungsi(x), ...) adalah fold expression (C++17) yang memperluas paket argumen menjadi satu pemanggilan per argumen.
  5. Baris 43-49 (counter yang di-reset): dua penghitung statis direset sebelum tiap pengukuran, sehingga angka di output murni milik pemanggilan yang sedang diuji — bukan akumulasi.

std::forward: Meneruskan Sesuai Asalnya #

std::forward<T>(arg) bisa dibaca sebagai “kembalikan arg dengan kualitas asalnya, sesuai yang diduksi T”: bila T adalah T& (berarti argumen lvalue), ia mengembalikan lvalue; bila T polos (berarti argumen rvalue), ia mengembalikan rvalue. Dengan itu, wrapper bisa jujur pada kedua jenis pemanggil. Kontras dengan std::move yang tegas: move selalu menghasilkan rvalue tanpa peduli asal — pas untuk saat kita sendiri yang menyerahkan objek, salah untuk wrapper yang hanya penerus.

Dua aturan menjaga kode forwarding tetap benar. Pertama, std::forward wajib ditulis dengan argumen template-nya — std::forward<Args>(a), bukan std::forward(a) — karena tanpa T ia tidak tahu kualitas asal yang harus dipulihkan. Kedua, T&& dan std::forward<T> adalah pasangan yang tidak boleh berpisah: parameter T&& tanpa forward menggugurkan rvalue-ness (jadi copy), sedangkan std::forward tanpa parameter T&& tidak pernah muncul sebagai pola. Praktik yang lazim: paket parameter forwarding hampir selalu ditulis Args&&... di deklarasi dan std::forward<Args>(...)... di setiap penerusan.

Pola emplace-style di Pustaka Standar #

Baris 35-38 meniru persis desain std::vector::emplace_back: alih-alih menerima objek jadi, fungsi menerima argumen constructor dan membangun elemen di tempatnya. Keuntungannya ganda — tidak ada objek perantara yang dibuat lalu dibuang, dan argumen rvalue tetap dipindahkan masuk. Bentuk penerusan ini ada di banyak titik standar: std::make_unique dan std::make_shared meneruskan argumen ke constructor objek, emplace/try_emplace di container asosiatif membangun elemen di tempat, dan std::thread meneruskan argumen ke fungsi thread-nya — semuanya satu pola yang kini bisa kita baca dengan lancar.

Fold expression (v.emplace_back(std::forward<Args>(a)), ...) memakai operator koma untuk mengeksekusi satu pemanggilan per elemen paket — cara C++17 mengganti perulangan rekursif gaya C++11. Untuk satu argumen tanpa paket, polanya lebih pendek: template <typename T> auto bungkus(T&& x) { return f(std::forward<T>(x)); }. Kapan pun kita menulis fungsi generik yang membungkus fungsi lain — logger, timer, cache, decorator — pola ini adalah tulang belakangnya.

Mekanisme penerusan argumen ini menutup bagian “mesin berat” modern C++: deduksi, move, dan forward saling melengkapi dalam satu desain yang konsisten. Chapter berikutnya berbalik ke sisi ergonomis — structured bindings, cara elegan membongkar isi struct, std::pair, std::tuple, dan std::array ke beberapa nama sekaligus, lengkap dengan aturan by value versus by reference-nya.

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