Pada Chapter Ranges dan Views kita sesekali menulis std::ranges::sort(katalog, {}, &Produk::harga) — comparator kosong diikuti alamat member — tanpa menjelaskan kedudukan kedua parameter itu. Chapter penutup Section ini melengkapi sisa peta: projection sebagai cara memberi kunci operasi tanpa lambda eksplisit, perbandingannya dengan comparator, lalu tiga fitur C++23 yang membuat pipeline ranges makin ringkas: std::ranges::to, std::views::zip, dan std::views::enumerate. Setelah chapter ini, Section Iterators dan Algorithms resmi tuntas.

Projection: Kunci Operasi Tanpa Lambda #

Hampir semua algoritma di namespace std::ranges menerima satu parameter tambahan di posisi paling akhir: projection. Projection adalah callable yang diterapkan pada setiap elemen sebelum elemen diteruskan ke logika algoritma — comparator, predikat, maupun operasi inti. Saat kita menulis std::ranges::sort(v, {}, &Produk::nama), algoritma membandingkan a.nama dengan b.nama memakai comparator default {}; elemen Produk tidak pernah diubah, tidak ada container sementara, dan tidak ada salinan nama yang kita tulis sendiri.

Mekanismenya: algoritma ranges menerapkan projection lewat std::invoke, sehingga pointer-to-member berfungsi langsung — &Produk::nama dipanggil sebagai elemen.*(&Produk::nama) untuk setiap elemen. Lambda juga sah sebagai projection dan berguna saat kunci berupa hasil perhitungan, misalnya stok * harga. Karena projection diterapkan pada posisi elemen saat algoritma berjalan, nilai asli container tetap utuh; yang dibandingkan, dicari, atau dihitung adalah hasil pemetaannya.

Comparator vs Projection #

Karena keduanya sama-sama callable, projection sering dikelirukan dengan comparator. Tugasnya berbeda: comparator membandingkan dua kunci, projection memetakan satu elemen menjadi kunci.

AspekComparatorProjection
Tugasmembandingkan dua kuncimemetakan satu elemen menjadi kunci
Bentuk panggilancomp(kunciA, kunciB)proj(elemen)
Jumlah argumenduasatu
Nilai kembaliboolkunci apa pun (int, std::string, dst.)
Urutan parametersebelum projectionpaling akhir
Contohstd::ranges::greater{}&Produk::harga

Pada std::ranges::sort(v, comp, proj), pasangan kunci proj(a) dan proj(b) dibandingkan lewat comp; comparator kosong {} berarti < pada kunci hasil projection. Projection tidak pernah menggantikan comparator — keduanya berpasangan: projection menentukan kunci apa yang dibandingkan, comparator menentukan urutannya. Keputusan praktisnya: kunci berupa satu member dipetakan lewat pointer-to-member karena paling ringkas dan langsung terbaca maksudnya; kunci berupa hasil perhitungan dipetakan lewat lambda; sedangkan pengurutan multi-kriteria tetap wilayah comparator lambda dengan pola dari Chapter Custom Comparator.

Program berikut menguji keempat pola tersebut pada satu data katalog:

cpp
#include <algorithm>
#include <functional>
#include <iostream>
#include <string>
#include <vector>

struct Produk {
    std::string nama;
    int stok;
    int harga;
};

void cetak(const std::vector<Produk>& gudang) {
    for (std::size_t i = 0; i < gudang.size(); ++i) {
        if (i > 0) std::cout << " | ";
        std::cout << gudang[i].nama << '(' << gudang[i].stok << ','
                  << gudang[i].harga << ')';
    }
    std::cout << '\n';
}

int main() {
    std::vector<Produk> gudang = {
        {"Buku", 12, 45000},
        {"Pena", 30, 3000},
        {"Agenda", 8, 60000},
        {"Penghapus", 25, 2000},
        {"Penggaris", 15, 5000},
    };

    // 1. Projection tanpa comparator eksplisit: urut berdasarkan nama
    std::ranges::sort(gudang, {}, &Produk::nama);
    std::cout << "Sort nama     : ";
    cetak(gudang);

    // 2. Projection + comparator: harga menurun lewat std::ranges::greater
    std::ranges::sort(gudang, std::ranges::greater{}, &Produk::harga);
    std::cout << "Sort harga    : ";
    cetak(gudang);

    // 3. Projection berupa lambda: kunci hasil perhitungan stok * harga
    std::ranges::sort(gudang, {},
                      [](const Produk& p) { return p.stok * p.harga; });
    std::cout << "Sort nilai    : ";
    cetak(gudang);

    // 4. Projection pada algoritma lain: cari stok terbesar tanpa loop manual
    auto stokTerbesar = std::ranges::max_element(gudang, {}, &Produk::stok);
    std::cout << "Stok terbesar : " << stokTerbesar->nama << " ("
              << stokTerbesar->stok << ")\n";
    return 0;
}

Output yang Dihasilkan:

text
Sort nama     : Agenda(8,60000) | Buku(12,45000) | Pena(30,3000) | Penggaris(15,5000) | Penghapus(25,2000)
Sort harga    : Agenda(8,60000) | Buku(12,45000) | Penggaris(15,5000) | Pena(30,3000) | Penghapus(25,2000)
Sort nilai    : Penghapus(25,2000) | Penggaris(15,5000) | Pena(30,3000) | Agenda(8,60000) | Buku(12,45000)
Stok terbesar : Pena (30)

Bedah baris demi baris:

  1. Baris 32 (std::ranges::sort(gudang, {}, &Produk::nama)): argumen {} adalah comparator default < untuk std::string, dan &Produk::nama menjadi projection. Katalog terurut alfabetis tanpa satu lambda pun.
  2. Baris 37 (harga menurun): std::ranges::greater{} dipasangkan dengan projection &Produk::harga; comparator menerima dua int harga, bukan dua Produk, sehingga perbandingan berlangsung pada kunci hasil projection.
  3. Baris 42-43 (lambda sebagai projection): kunci p.stok * p.harga dihitung pada setiap pembandingan. Urutan berubah menjadi naik berdasarkan nilai persediaan, dan elemen asli tetap utuh.
  4. Baris 48 (std::ranges::max_element(gudang, {}, &Produk::stok)): projection bukan milik sort saja — max_element, min_element, find_if, hingga count_if versi ranges menerima parameter yang sama. Hasilnya iterator ke produk berstok 30 tanpa loop manual.
  5. Baris 13-20 (helper cetak): menampilkan setiap produk sebagai nama(stok,harga) agar isi katalog pada tiap percobaan terbaca jelas; helper ini tidak berubah sepanjang program.

ranges::to: Dari Pipeline ke Container #

Sebelum C++23, menyimpan hasil view ke container selalu lewat idiom penyalinan: deklarasikan tujuan kosong, lalu std::ranges::copy(pipeline, std::back_inserter(tujuan)) — dua langkah dan dua nama variabel. C++23 menambahkan std::ranges::to yang membentuk container langsung di ujung pipeline: nilai | std::views::filter(...) | std::ranges::to<std::vector>(). Tipe elemen disimpulkan otomatis dari pipeline, dan bila ingin eksplisit kita bisa menulis std::ranges::to<std::vector<int>>(). Tujuannya tidak terbatas pada vector — std::deque, std::list, std::string, hingga std::map dapat dipakai selama elemen hasil pipeline cocok dengan jenis containernya.

zip dan enumerate #

Dua view C++23 lain yang sering menggantikan loop indeks adalah std::views::zip dan std::views::enumerate. zip(r1, r2, ...) memasangkan elemen ke-i dari setiap range menjadi satu tuple; panjang hasil mengikuti range terpendek, sehingga dua rentang yang beda panjang tetap bisa dirangkai. enumerate(r) adalah kasus khusus yang nyaman: setiap elemen dipasangkan dengan indeksnya mulai dari 0, sehingga range-for yang butuh nomor urut tidak lagi memelihara variabel std::size_t i manual.

Keduanya lazy seperti view lain: elemen dipasangkan saat diiterasi, bukan saat dirangkai. Hasil zip dan enumerate sama-sama tuple, sehingga structured binding auto&& [a, b] adalah cara standar membacanya. Program terakhir Section ini merangkum ketiganya — zip, enumerate, dan ranges::to — dalam satu alur uji:

cpp
#include <algorithm>
#include <iostream>
#include <ranges>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> peserta = {"Budi", "Siti", "Dewi", "Rian"};
    std::vector<int> skor = {88, 92, 75};

    // 1. zip: pasangkan elemen ke-i tiap range; berhenti di range terpendek
    std::cout << "zip:\n";
    for (auto&& [nama, poin] : std::views::zip(peserta, skor)) {
        std::cout << "  " << nama << " -> " << poin << '\n';
    }

    // 2. enumerate: setiap elemen dipasangkan dengan indeksnya
    std::vector<std::string> tugas = {"bersih", "urut", "uji"};
    std::cout << "enumerate:\n";
    for (auto&& [indeks, nama] : std::views::enumerate(tugas)) {
        std::cout << "  " << indeks << ": " << nama << '\n';
    }

    // 3. ranges::to: materialisasi hasil pipeline ke container
    std::vector<int> nilai = {70, 55, 90, 62, 88, 45};
    auto lulus = nilai | std::views::filter([](int x) { return x >= 70; })
                       | std::ranges::to<std::vector>();
    std::cout << "lulus: ";
    bool pertama = true;
    for (int x : lulus) {
        if (!pertama) std::cout << ' ';
        std::cout << x;
        pertama = false;
    }
    std::cout << " (ukuran " << lulus.size() << ")\n";
    return 0;
}

Output yang Dihasilkan:

text
zip:
  Budi -> 88
  Siti -> 92
  Dewi -> 75
enumerate:
  0: bersih
  1: urut
  2: uji
lulus: 70 90 88 (ukuran 3)

Bedah baris demi baris:

  1. Baris 8-9 dan 13 (zip dua vector): peserta berisi empat nama, skor hanya tiga angka. Zip menghasilkan tiga pasangan — Rian tidak muncul karena range terpendek menentukan panjang hasil.
  2. Baris 20 (enumerate): indeks bertipe tanda beda rentang (std::ptrdiff_t) dan nama merujuk elemen asli, sehingga nomor urut dan isi terbaca tanpa variabel counter.
  3. Baris 26-27 (pipeline + ranges::to): pipeline filter nilai minimal 70 langsung dikumpulkan menjadi vector lulus tanpa std::ranges::copy dan tanpa deklarasi vector kosong terlebih dahulu.
  4. Baris 29-34 (pencetak pemisah-depan): pola if (!pertama) menampilkan spasi hanya antar elemen, sehingga baris lulus: tidak berakhir dengan spasi — masalah pembatas yang kita temui pada ostream_iterator.
  5. Baris 35 (ukuran hasil): lulus.size() bernilai 3, membuktikan ranges::to menghasilkan container sungguhan yang bisa dihitung, diakses, dan disimpan.

Penutup Section: dari begin() hingga C++23 #

Section Iterators dan Algorithms kini benar-benar tuntas. Kita berangkat dari konsep iterator dan lima kategorinya, melewati begin()/end(), lalu memakai seluruh algoritma inti — sort, find, binary search, min/max, count, copy/move, transform, idiom remove-erase, partition, accumulate, dan algoritma <numeric> — sebelum menyempurnakan perbandingan lewat custom comparator dan function objects. Ranges dan views merapikan boilerplate, dan chapter ini menutup sisanya: projection untuk kunci, ranges::to untuk hasil, serta zip dan enumerate untuk penggabungan. Fondasi iterator yang kita bangun tetap bekerja di balik semua fitur baru tersebut.

Langkah berikutnya natural: data yang kita olah selama ini selalu ada di memori, sedangkan program sungguhan menerima dan mengirim data lewat stream. Section berikutnya, Input-Output dan File, dibuka dengan Chapter Peta iostream yang membedah keluarga std::istream dan std::ostream — dua kelas yang sejak awal kita pakai melalui std::cin dan std::cout, bahkan lewat istream_iterator dan ostream_iterator pada Chapter ini.

Rujukan resmi: std::ranges::sort , std::ranges::to , dan std::views::zip .