Mengirim container ke fungsi

Konvensi const reference, kebiasaan efisiensi, dan jebakan penyalinan diam-diam.

Daftar isi

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.

cpp biaya-salin.cpp
#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;
}
text Keluaran
Memanggil jumlahTotal dengan pass by value
  elemen bernilai 10 disalin
  elemen bernilai 20 disalin
  elemen bernilai 30 disalin
Total: 60

Tiga 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.

cpp konvensi-const-ref.cpp
#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;
}
text Keluaran
Banyak nilai: 3
Total: 245

Fungsi 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.

TujuanBentuk yang lazimBiaya penyalinan
Membaca isi containerconst std::vector<T>&Tidak ada
Mengubah isi containerstd::vector<T>&Tidak ada
Menyimpan salinan milik fungsi sendiristd::vector<T> (by value)Salinan penuh
Mengembalikan hasil dari fungsistd::vector<T> sebagai nilai kembalianBiasanya 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.

  1. 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.
  2. Menyalin elemen pada perulangan dengan for (auto x : data) untuk tipe elemen yang besar. Tulis const auto& apabila elemennya hanya dibaca, atau auto& apabila elemennya diubah.
  3. Mengirim parameter std::vector<T>& tanpa const untuk fungsi yang hanya membaca. Kompilasinya sah, tetapi kontraknya menyesatkan, karena pembaca tanda tangan menyangka fungsinya mengubah isi. Tambahkan const agar 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 bentuk std::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#