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 #
| Sintaks | Nama | Copy? | Modifikasi variabel pemanggil? |
|---|---|---|---|
void f(T x) | Pass-by-value | Ya | Tidak (x adalah salinan) |
void f(T& x) | Pass-by-reference | Tidak | Ya |
void f(const T& x) | Pass-by-const-reference | Tidak | Tidak (read-only) |
void f(T&& x) | Pass-by-rvalue-reference | Tidak (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:
- Tipe kecil (
int,double, pointer, small struct ≤ 16 byte). - Fungsi akan menyimpan argumen (mis.
push_back) — sink. DenganT x, kita dapat salinan sendiri; tinggalstd::moveke tempat penyimpanan.
Kode: Tiga Mode + Benchmark #
#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):
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 msBedah Kode Baris-per-Baris #
int sum_by_value(int a, int b): parameterintdi-copy. Untuk tipe primitif ≤ 8 byte, copy secepat pass-by-reference (kadang lebih cepat karena tidak perlu indirection). Default untukint,double,pointer.void double_value(int& x): parameterint&— modifikasixdi dalam fungsi mengubah variabel pemanggil. Setelahdouble_value(x)denganx = 5, nilaixmenjadi10.size_t length_of(const std::string& s):const &— tanpa copy, read-only.constmemberi jaminan kompiler bahwastidak akan dimutasi; pemanggil aman melewatkanconst std::stringsekalipun.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 direturn.take_string(std::string s)dengantake_string(std::move(owned)):std::movemeng-castownedke rvalue, sehingga move constructor dipilih alih-alih copy constructor.owneddipindahkan — sumbernya (heap pointer) ditransfer ke parameters;ownedmenjadi string kosong tanpa alokasi/dealokasi.owned.size()setelah move:0. Setelahstd::move, objek sumber berada dalam valid but unspecified state — untukstd::string, biasanya kosong. Hindari mengakses objek yang sudah di-move kecuali untuk re-assignment atau destruct.- 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 #
| Skenario | Pilihan |
|---|---|
int, double, pointer, enum | T x (by-value) |
| String/container besar, read-only | const 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 range | Iterator first, Iterator last (by-value) |
| Lambda capture | [&] by-ref, [=] by-value |
| Ownership transfer penuh | T 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”:
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.