C++ mengizinkan tiga cara melewatkan argumen ke fungsi: by-value (copy), by-reference (&), dan by-const-reference (const &). Pilihan ini bukan sekadar soal sintaks — ia menentukan apakah salinan data dibuat (mempengaruhi performa) dan apakah variabel pemanggil bisa dimodifikasi (mempengaruhi kebenaran). Untuk tipe kecil seperti int, copy murah; untuk std::vector berukuran sejuta elemen, copy bisa memakan ratusan milidetik. Pada Chapter ini kita akan memetakan tiga mode passing, kapan masing-masing tepat, dan bagaimana std::move mengubah persamaan untuk ownership transfer.

Tiga Mode Passing #

SintaksNamaCopy?Modifikasi variabel pemanggil?
void f(T x)Pass-by-valueYaTidak (x adalah salinan)
void f(T& x)Pass-by-referenceTidakYa
void f(const T& x)Pass-by-const-referenceTidakTidak (read-only)
void f(T&& x)Pass-by-rvalue-referenceTidak (move)Pindahkan resource

Mekanisme: Kapan Copy Terjadi #

Saat kita menulis void f(std::string s) dan memanggil f(my_string), compiler memanggil copy constructor std::string — mengalokasikan buffer baru, menyalin seluruh karakter. Untuk string 10 MB, ini berarti alokasi heap + memcpy 10 MB.

Saat kita menulis void f(const std::string& s), compiler meneruskan alamat my_string. Tidak ada alokasi, tidak ada copy. s adalah alias ke my_string.

Pass-by-value menjadi pilihan yang baik bila:

  1. Tipe kecil (int, double, pointer, small struct ≤ 16 byte).
  2. Fungsi akan menyimpan argumen (mis. push_back) — sink. Dengan T x, kita dapat salinan sendiri; tinggal std::move ke tempat penyimpanan.

Kode: Tiga Mode + Benchmark #

cpp
#include <iostream>
#include <vector>
#include <string>
#include <chrono>

// Pass-by-value: salinan dibuat; cocok untuk tipe kecil
int sum_by_value(int a, int b) {
    return a + b;
}

// Pass-by-const-reference: tanpa copy; cocok untuk tipe besar yang hanya dibaca
size_t length_of(const std::string& s) {
    return s.size();
}

// Pass-by-reference (non-const): modifikasi variabel pemanggil
void double_value(int& x) { x *= 2; }

// Pass-by-value + std::move untuk ownership transfer
void take_string(std::string s) {
    std::cout << "took string of size " << s.size() << '\n';
}

// Pass-by-const-reference — output via return
std::string concat(const std::string& a, const std::string& b) {
    return a + b;
}

// Compare cost: by-value vs const-ref for std::string
void benchmark() {
    std::string big(10'000'000, 'x');  // 10 MB string

    const int N = 1000;
    auto t1 = std::chrono::high_resolution_clock::now();
    volatile size_t acc1 = 0;
    for (int i = 0; i < N; ++i) {
        // by-value: salin 10 MB setiap kali
        std::string copy = big;
        acc1 += copy.size();
    }
    auto t2 = std::chrono::high_resolution_clock::now();

    auto t3 = std::chrono::high_resolution_clock::now();
    volatile size_t acc2 = 0;
    for (int i = 0; i < N; ++i) {
        // const-ref: tidak ada copy
        acc2 += length_of(big);
    }
    auto t4 = std::chrono::high_resolution_clock::now();

    std::chrono::duration<double, std::milli> d1 = t2 - t1;
    std::chrono::duration<double, std::milli> d2 = t4 - t3;
    std::cout << "by-value (copy 10MB x" << N << "): " << d1.count() << " ms\n";
    std::cout << "const-ref (no copy) x"     << N << ": " << d2.count() << " ms\n";
}

int main() {
    int x = 5;
    std::cout << "sum_by_value(3,4) = " << sum_by_value(3, 4) << '\n';
    double_value(x);
    std::cout << "after double_value(x=5): x = " << x << '\n';

    std::string s = "hello world";
    std::cout << "length_of(s) = " << length_of(s) << '\n';

    std::string a = "foo", b = "bar";
    std::string c = concat(a, b);
    std::cout << "concat(\"foo\", \"bar\") = " << c << '\n';

    // Pass-by-value + std::move: transfer kepemilikan
    std::string owned = "owned-data";
    take_string(std::move(owned));
    std::cout << "after move, owned.size() = " << owned.size() << " (dipindahkan)\n";

    benchmark();
    return 0;
}

Output yang Dihasilkan (running lokal):

text
sum_by_value(3,4) = 7
after double_value(x=5): x = 10
length_of(s) = 11
concat("foo", "bar") = foobar
took string of size 10
after move, owned.size() = 0 (dipindahkan)
by-value (copy 10MB x1000): 891.127 ms
const-ref (no copy) x1000: 0.002087 ms

Bedah Kode Baris-per-Baris #

  1. int sum_by_value(int a, int b): parameter int di-copy. Untuk tipe primitif ≤ 8 byte, copy secepat pass-by-reference (kadang lebih cepat karena tidak perlu indirection). Default untuk int, double, pointer.
  2. void double_value(int& x): parameter int& — modifikasi x di dalam fungsi mengubah variabel pemanggil. Setelah double_value(x) dengan x = 5, nilai x menjadi 10.
  3. size_t length_of(const std::string& s): const & — tanpa copy, read-only. const memberi jaminan kompiler bahwa s tidak akan dimutasi; pemanggil aman melewatkan const std::string sekalipun.
  4. std::string concat(const std::string& a, const std::string& b): idiomatik untuk fungsi yang membaca dan mengembalikan hasil baru. Tidak ada copy input; satu string baru dibuat di return.
  5. take_string(std::string s) dengan take_string(std::move(owned)): std::move meng-cast owned ke rvalue, sehingga move constructor dipilih alih-alih copy constructor. owned dipindahkan — sumbernya (heap pointer) ditransfer ke parameter s; owned menjadi string kosong tanpa alokasi/dealokasi.
  6. owned.size() setelah move: 0. Setelah std::move, objek sumber berada dalam valid but unspecified state — untuk std::string, biasanya kosong. Hindari mengakses objek yang sudah di-move kecuali untuk re-assignment atau destruct.
  7. Benchmark copy vs const-ref: copy 10 MB string 1000 kali butuh ~891 ms (891 µs per copy — ~89 GB/s memcpy). const-ref hanya 2 µs total — 400.000× lebih cepat. Perbedaan ini menjadi dramatis saat kontainer berukuran besar.

Aturan Praktis Pemilihan #

SkenarioPilihan
int, double, pointer, enumT x (by-value)
String/container besar, read-onlyconst T& x
Output parameter (modifikasi pemanggil)T& x (out) atau T& x (in-out)
Fungsi menyimpan argumen (sink, seperti push_back)T x lalu std::move(x) ke tujuan
Iterator rangeIterator first, Iterator last (by-value)
Lambda capture[&] by-ref, [=] by-value
Ownership transfer penuhT x atau T&& x (pakai std::move saat pemanggilan)

Jebakan Umum #

Slicing pada Pass-by-Value #

Bila T adalah kelas base dan kita pass turunan Derived by-value, hanya bagian Base yang disalin — bagian Derived “dipotong”:

cpp
struct Base { int b; };
struct Derived : Base { int d; };
void f(Base x) { /* tidak ada x.d */ }
Derived der{1, 2};
f(der);  // slicing: x.b=1, x.d hilang

Solusi: gunakan const Base& (atau const Derived& bila tahu tipenya) — tidak ada slicing.

Modifikasi Tidak Disengaja via T& #

void f(std::vector<int>& v) mengizinkan mutasi. Bila Anda hanya membaca, gunakan const std::vector<int>& — kompiler akan menangkap modifikasi tak sengaja sebagai error.

Ringkasan #

  • Tiga mode utama: by-value (copy), by-reference (alias, mutable), by-const-reference (alias, read-only).
  • Aturan default: by-value untuk tipe kecil ≤ 16 byte; const T& untuk tipe besar yang hanya dibaca; T& untuk output parameter.
  • std::move + pass-by-value mengoptimalkan sink: satu salinan (bila lvalue) atau satu move (bila rvalue) — tanpa perlu dua overload.
  • Hindari slicing: jangan pass turunan by-value ke parameter base.
  • Untuk argumen container besar, const T& menghemat copy yang bisa berarti perbedaan ms vs detik.