Topik ini membahas cara mengirim container seperti std::vector dan std::string ke fungsi beserta konvensi parameternya. Konvensi const std::vector<int>& sesungguhnya sudah muncul pada proyek header sebelumnya, dan topik ini menjelaskan alasannya secara utuh, mulai dari biaya penyalinan yang tersembunyi sampai kebiasaan efisiensi yang lazim. Setelah membaca topik ini, bentuk parameter untuk membaca, mengubah, dan menerima container sudah dapat dipilih dengan dasar yang jelas.
Penyalinan diam-diam pada pass by value#
Parameter yang dideklarasikan tanpa & menerima salinan argumen, dan aturan ini berlaku sama pada container. Apabila fungsi menerima std::vector secara pass by value, seluruh vector disalin, yang berarti alokasi memori baru beserta penyalinan setiap elemennya. Penyalinan ini terjadi tanpa pesan apa pun, sehingga hasil program tetap benar tetapi waktu dan memori terbuang pada setiap pemanggilan.
Program berikut membuat elemen yang melapor setiap kali disalin. Struct Nilai memiliki copy constructor yang mencetak pesan, sedangkan move constructor-nya sengaja dibuat senyap agar yang terlihat murni penyalinan akibat pengiriman argumen.
#include <iostream>
#include <vector>
struct Nilai {
int angka;
Nilai(int a) : angka(a) {}
Nilai(const Nilai& lain) : angka(lain.angka) {
std::cout << " elemen bernilai " << angka << " disalin\n";
}
Nilai(Nilai&& lain) noexcept : angka(lain.angka) {}
};
int jumlahTotal(std::vector<Nilai> data) {
int total = 0;
for (const Nilai& n : data) {
total += n.angka;
}
return total;
}
int main() {
std::vector<Nilai> daftar;
daftar.push_back(Nilai(10));
daftar.push_back(Nilai(20));
daftar.push_back(Nilai(30));
std::cout << "Memanggil jumlahTotal dengan pass by value\n";
int total = jumlahTotal(daftar);
std::cout << "Total: " << total << '\n';
return 0;
}Memanggil jumlahTotal dengan pass by value
elemen bernilai 10 disalin
elemen bernilai 20 disalin
elemen bernilai 30 disalin
Total: 60Tiga baris pesan muncul persis saat jumlahTotal dipanggil, karena penyalinan vector menyalin ketiga elemennya satu per satu. Pada program nyata dengan vector berisi ribuan std::string, biaya yang sama berulang pada setiap pemanggilan tanpa manfaat apa pun, karena fungsi tersebut hanya membaca isinya. Perhatikan pula bahwa push_back(Nilai(10)) tidak mencetak apa pun, karena memasukkan objek sementara memindahkan isinya, bukan menyalinnya.
Besarnya biaya itu mudah diperkirakan dari ukuran datanya. Satu pemanggilan dengan vector berisi seribu std::string menyalin seribu string, dan penyalinan satu string dapat menyalin puluhan hingga ratusan karakter. Pada fungsi yang dipanggil berulang, misalnya sekali per baris laporan, biaya yang sama terbayar berkali-kali tanpa menghasilkan nilai tambahan apa pun.
Konvensi const std::vector&#
Bentuk parameter const std::vector<T>& menyambungkan parameter ke vector aslinya tanpa penyalinan apa pun, dan kata const melarang perubahan di dalam fungsi. Kombinasi inilah konvensi default untuk fungsi yang hanya membaca container, sebagaimana const std::string& pada topik pengiriman argumen. Apabila fungsi memang harus mengubah isinya, bentuk std::vector<T>& tanpa const dipakai agar maksud itu terbaca dari tanda tangannya.
#include <iostream>
#include <vector>
int jumlahTotal(const std::vector<int>& data) {
int total = 0;
for (int n : data) {
total += n;
}
return total;
}
void isiNilai(std::vector<int>& keluaran) {
keluaran.push_back(70);
keluaran.push_back(85);
keluaran.push_back(90);
}
int main() {
std::vector<int> nilai;
isiNilai(nilai);
std::cout << "Banyak nilai: " << nilai.size() << '\n';
std::cout << "Total: " << jumlahTotal(nilai) << '\n';
return 0;
}Banyak nilai: 3
Total: 245Fungsi isiNilai menerima std::vector<int>& sehingga push_back() menyentuh vector nilai milik pemanggil, dan hasilnya terlihat pada cetakan pertama. Fungsi jumlahTotal menerima const std::vector<int>&, sehingga pemanggilannya tidak menyalin satu elemen pun. Apabila tipe elemen pada contoh sebelumnya digunakan lagi di sini, tidak ada satu pesan penyalinan pun yang akan muncul.
Konvensi ini berlaku seragam untuk container lain. Parameter pembaca berbentuk const std::string& untuk teks, const std::array<T, N>& untuk array tetap, dan const std::vector<T>& untuk vector, sedangkan bentuk yang mengubah isi membuang const-nya. Bentuk const reference juga menerima objek sementara, misalnya hasil fungsi lain yang dikirim langsung, tanpa perubahan apa pun di sisi pemanggil.
Kebiasaan efisiensi di sekitar container#
Tabel berikut merangkum kebiasaan yang lazim pada penulisan parameter dan nilai kembalian container.
| Tujuan | Bentuk yang lazim | Biaya penyalinan |
|---|---|---|
| Membaca isi container | const std::vector<T>& | Tidak ada |
| Mengubah isi container | std::vector<T>& | Tidak ada |
| Menyimpan salinan milik fungsi sendiri | std::vector<T> (by value) | Salinan penuh |
| Mengembalikan hasil dari fungsi | std::vector<T> sebagai nilai kembalian | Biasanya dihilangkan compiler |
Dua baris terakhir perlu penjelasan. Parameter by value hanya tepat apabila fungsi memang memerlukan salinan miliknya sendiri, misalnya untuk dimodifikasi tanpa menyentuh aslinya, dan kasus semacam ini jarang muncul pada kode awal. Sebaliknya, mengembalikan vector dari fungsi secara by value justru merupakan kebiasaan yang benar. Compiler biasanya menghilangkan penyalinan hasil tersebut melalui optimasi copy elision, sehingga vector yang dirakit di dalam fungsi tidak dikenai biaya penyalinan ganda.
Kebiasaan efisien lain terletak pada urutan penyusunan data. Apabila fungsi membangun vector besar secara bertahap, panggilah reserve() terlebih dahulu sebagaimana dijelaskan pada topik std::vector, kemudian kembalikan hasilnya secara by value. Rangkaian tersebut menghasilkan satu alokasi utama tanpa salinan akhir, sehingga biaya totalnya mendekati biaya pengisian data itu sendiri.
Kebiasaan yang sama berlaku pada perulangan. Bentuk for (auto x : data) menyalin setiap elemen ke variabel x, sedangkan for (const auto& x : data) membaca elemen aslinya tanpa salinan. Pada elemen bertipe kecil seperti int, selisihnya tidak terasa, tetapi kebiasaan menuliskan & sejak awal melindungi kode dari penyalinan yang tidak perlu ketika tipe elemennya berganti menjadi std::string.
Kesalahan umum#
Tiga kesalahan berikut paling sering muncul ketika container dikirim ke fungsi.
- Lupa menambahkan
&pada parameter yang semestinya reference. Fungsi tetap berjalan dan hasil perhitungannya benar, tetapi setiap pemanggilan menyalin seluruh container, dan perubahan yang dilakukan fungsi terhadap parameternya lenyap bersama salinan itu. Periksa tanda tangan fungsinya terlebih dahulu ketika perubahan tidak terlihat oleh pemanggil. - Menyalin elemen pada perulangan dengan
for (auto x : data)untuk tipe elemen yang besar. Tulisconst auto&apabila elemennya hanya dibaca, atauauto&apabila elemennya diubah. - Mengirim parameter
std::vector<T>&tanpaconstuntuk fungsi yang hanya membaca. Kompilasinya sah, tetapi kontraknya menyesatkan, karena pembaca tanda tangan menyangka fungsinya mengubah isi. Tambahkanconstagar komitmen fungsi dinyatakan secara eksplisit.
Ketiganya serupa dalam gejalanya, yaitu program tetap berjalan dengan hasil yang benar tetapi membawa biaya yang tidak perlu atau kontrak yang menyesatkan. Pemeriksaan yang murah adalah membaca kembali tanda tangan fungsi dan kepala perulangannya, lalu mencari parameter serta variabel perulangan yang tidak memakai &. Perbaikannya pun singkat, yaitu menambahkan const dan & pada tempat yang semestinya.
Ringkasan#
- Parameter container tanpa
&menyalin seluruh isinya pada setiap pemanggilan, dan penyalinan itu terjadi tanpa pesan apa pun. - Konvensi default untuk membaca container adalah
const std::vector<T>&, sedangkan bentukstd::vector<T>&disisakan untuk fungsi yang memang mengubah isinya. - Mengembalikan vector secara by value merupakan kebiasaan yang benar karena compiler biasanya menghilangkan penyalinannya, sedangkan
for (const auto& x : data)menghindari penyalinan per elemen.
Referensi#
- Reference declaration — cppreference.com — Kaidah deklarasi reference dan hubungannya dengan objek yang dirujuk.
- std::vector::at — cppreference.com — Akses elemen dengan pemeriksaan batas pada vector.
- Pass by lvalue reference — LearnCpp — Pengiriman argumen besar tanpa penyalinan.
- Pass by const lvalue reference — LearnCpp — Konvensi const reference untuk argumen yang hanya dibaca.