Dua nilai, satu objek
Kadang kamu perlu menyimpan dua hal bersama-sama: nama dan skor, x dan y, penanda "berhasil atau tidak" dan hasilnya. Kamu bisa mendefinisikan struct untuk setiap pasangan seperti itu, tapi untuk pengelompokan sekali pakai itu terlalu repot. std::pair (dari <utility>) menggabungkan tepat dua nilai ke dalam satu objek, dan std::tuple (dari <tuple>) memperluas ide itu ke sejumlah nilai yang tetap.
Kamu sebenarnya sudah bertemu std::pair secara tidak langsung: setiap elemen std::map adalah pair<const Key, Value>. Halaman ini membahasnya secara terang dan menunjukkan cara modern yang mudah dibaca untuk membuat dan membongkar tipe-tipe ini.
Kedua member-nya selalu bernama .first dan .second, dan tidak mengikuti nama variabelmu. Itulah harga dari tipe generik: nama field-nya menunjukkan posisi, bukan arti.
Membuat pair
Ada tiga cara umum untuk membuat pair, dan semuanya menghasilkan objek yang sama.
make_pair menebak tipe elemen untukmu, dan itu berguna sebelum C++17. Sekarang inisialisasi dengan kurung kurawal ditambah class template argument deduction (pair p{"Boris", 85};) sudah cukup untuk sebagian besar kasus, tapi kamu masih akan melihat make_pair di banyak kode yang sudah ada.
Ada satu jebakan soal deduksi tipe: make_pair("hi", 3) menghasilkan pair<const char*, int>, bukan pair<string, int>. String literal bukan std::string. Kalau kamu butuh string, tulis secara eksplisit, misalnya make_pair(string("hi"), 3), atau tulis lengkap tipe pair-nya. Kalau tidak, nanti kamu bisa mendapat hasil perbandingan atau penyalinan yang mengejutkan.
Membongkar dengan structured binding
Membaca .first dan .second di mana-mana cepat membuat kode sulit dibaca, karena namanya tidak memberi tahu apa pun. Structured binding di C++17 memungkinkan kamu memberi nama sungguhan untuk kedua field itu dalam satu baris:
Fitur ini sangat berguna di for berbasis range yang menelusuri map, karena setiap elemennya adalah pair. Alih-alih it->first / it->second, kamu langsung memberi nama key dan value-nya:
Pakai const auto& di dalam loop, sama seperti untuk elemen container lainnya. Cara ini menghindari penyalinan setiap pair dan menunjukkan bahwa kamu hanya membaca. Kalau & dihapus, setiap entri akan disalin, dan itu bug performa yang diam-diam terjadi pada map yang besar.
Kalau dua tidak cukup: tuple
pair hanya sampai dua nilai. Kalau kamu butuh tiga atau lebih, std::tuple adalah ide yang sama dengan jumlah bebas. Kamu membuatnya dengan inisialisasi kurung kurawal atau make_tuple, lalu membacanya dengan std::get<N>, dengan N berupa indeks yang diketahui saat kompilasi.
Indeks di dalam get<> harus konstanta yang diketahui saat kompilasi. get<i>(record) dengan i berupa variabel runtime tidak akan bisa dikompilasi. Field tuple bisa punya tipe yang berbeda-beda, jadi tipe elemennya harus ditentukan saat kompilasi, bukan saat program berjalan. Kalau kamu ternyata butuh indeks runtime, kemungkinan yang kamu perlukan adalah vector.
Structured binding juga bisa dipakai untuk tuple, dan inilah cara yang mudah dibaca untuk memakainya:
Mengembalikan banyak nilai
Alasan sehari-hari memakai tipe-tipe ini adalah untuk mengembalikan lebih dari satu nilai dari sebuah fungsi tanpa membuat struct baru atau repot dengan parameter output. Gabungkan hasilnya ke dalam pair atau tuple, lalu bongkar di tempat pemanggilannya.
Untuk tiga hasil atau lebih, kembalikan tuple dengan cara yang sama. Ada juga std::tie, trik lama yang membongkar nilai ke variabel yang sudah ada, bukan mendeklarasikan variabel baru. Ini berguna saat kamu ingin mengabaikan sebuah field dengan std::ignore:
Tapi tahu kapan berhenti: kalau kelompok field yang sama muncul di lebih dari satu tempat, atau kamu terus lupa apakah .second itu skor atau jumlah, definisikan struct dengan member yang punya nama. pair dan tuple paling cocok untuk pengelompokan lokal yang berumur pendek; field bernama lebih baik begitu datanya hidup lebih lama daripada satu ekspresi.
Membandingkan dan mengurutkan
Ada bonus yang berguna: pair dan tuple sudah punya operator perbandingan bawaan yang bekerja secara leksikografis. Mereka membandingkan elemen pertama, dan baru beralih ke elemen berikutnya kalau elemen pertamanya sama. Ini membuat mereka sempurna sebagai kunci untuk pengurutan.
Perhatikan bahwa urutan field-nya penting: menaruh age di depan berarti data diurutkan terutama berdasarkan umur, lalu berdasarkan nama kalau umurnya sama. Kalau kamu ingin mengurutkan berdasarkan nama dulu, tukar urutan elemen tuple-nya. Perbandingan bawaan inilah yang membuat pair<priority, item> jadi pola yang umum untuk priority queue.
Berikutnya: iterator
Kamu sudah melihat .first, .second, it->first, dan *it muncul di sekitar container. Yang sebenarnya menghubungkan elemen pair dengan map tempatnya berada adalah iterator. Halaman berikutnya membahas iterator dengan tuntas: apa yang sebenarnya dikembalikan begin() dan end(), bagaimana ++it menelusuri container, dan jebakan iterator invalidation yang menyebabkan beberapa undefined behavior paling parah di C++.
Pertanyaan yang sering diajukan
Apa perbedaan pair dan tuple di C++?
std::pair menyimpan tepat dua nilai, yang diakses dengan .first dan .second. std::tuple menyimpan sejumlah nilai yang tetap (nol, dua, tiga, atau lebih), yang diakses dengan std::get<N>(t). Pada dasarnya pair adalah tuple dua elemen dengan nama member yang lebih ramah; pakai tuple hanya kalau kamu butuh tiga field atau lebih.
Bagaimana cara mengakses elemen tuple di C++?
Pakai std::get<N>(t) dengan indeks yang diketahui saat kompilasi, misalnya std::get<0>(t). Sejak C++17 kamu juga bisa membongkarnya dengan structured binding: auto [a, b, c] = t; memberi setiap elemen variabelnya sendiri. Kamu tidak bisa mengakses tuple dengan indeks berupa variabel runtime, karena std::get<i> membutuhkan i yang konstan.
Bagaimana cara mengembalikan banyak nilai dari fungsi di C++?
Kembalikan std::pair atau std::tuple, lalu bongkar di tempat pemanggilannya dengan structured binding: auto [ok, value] = parse(text);. Cara ini lebih rapi daripada parameter output dan tidak perlu mendefinisikan struct sekali pakai. Tapi struct dengan nama lebih mudah dibaca kalau field-nya dipakai lebih lama daripada satu pemanggilan.