Sepanjang Section ini, setiap panggilan algoritma selalu diawali pasangan begin() dan end(), dan pengolahan bertahap memerlukan container sementara untuk menampung hasil antara. C++20 menghadirkan jawaban modern bernama Ranges library: satu header <ranges> berisi view adaptor yang dapat dirangkai seperti pipa, ditambah versi baru semua algoritma di namespace std::ranges. View adalah cara melihat data tanpa menyalinnya — view tidak menyimpan elemen dan tidak menghitung apa pun saat dirangkai. Pekerjaan baru dilakukan ketika hasil pipeline diiterasi, satu elemen demi satu elemen. Chapter ini menutup Section Iterators dan Algorithms: kita merangkai view menjadi pipeline, membuktikan lazy evaluation dengan deret tak hingga, membandingkan gaya ranges dengan gaya iterator klasik, lalu merangkum perjalanan Section ini.

Apa Itu Range dan View #

Range (rentang) adalah istilah umum untuk segala sesuatu yang bisa diiterasi dengan begin() dan end() — std::vector, std::string, array, bahkan hasil view. Definisi ini dijaga oleh concept std::ranges::range sehingga algoritma versi ranges dapat menerima semua jenis sumber data tersebut. View (lebih tepatnya range adaptor) adalah range yang ringan: ia tidak memiliki elemen sendiri, melainkan menyimpan referensi ke range lain beserta satu aturan transformasi. std::views::filter(predikat) misalnya menghasilkan view yang saat diiterasi hanya melewatkan elemen yang lolos predikat, tanpa memindahkan satu byte pun dari sumbernya.

Beberapa adaptor yang paling sering dipakai:

  • std::views::filter(p) — hanya melewatkan elemen yang lolos predikat p
  • std::views::transform(f) — melewatkan hasil f(x) untuk setiap elemen x
  • std::views::take(n) / std::views::drop(n) — mengambil atau membuang n elemen pertama
  • std::views::iota(a, b) — menghasilkan deret a, a+1, … hingga sebelum b; tanpa b, deretnya tak hingga
  • std::views::reverse — membalik urutan elemen

Adaptor dirangkai dengan operator pipe |: ekspresi di kiri menjadi masukan adaptor di kanan, sehingga pipeline panjang dibaca dari kiri ke kanan persis mengikuti urutan pengolahan. Semua view dideklarasikan di header <ranges>, sedangkan algoritma versi ranges tetap tinggal di <algorithm> dan <numeric> yang sudah kita kenal. Catatan penting: merangkai view tidak pernah mengubah container sumber — sifat yang sama dengan algoritma klasik yang membaca data lewat iterator.

Pipeline Views dengan Sintaks Pipe #

Program berikut menyatukan materi inti chapter ini dalam satu file: satu pipeline tiga adaptor di atas vector, satu pipeline di atas deret tak hingga, kombinasi drop dan reverse, serta penutup berupa std::ranges::sort:

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

bool prima(int n) {
    if (n < 2) return false;
    for (int i = 2; i * i <= n; ++i) {
        if (n % i == 0) return false;
    }
    return true;
}

// Helper cetak untuk rentang apa pun. Parameternya R&& (bukan const R&)
// karena banyak view C++20 tidak bisa diiterasi lewat referensi const.
template <typename R>
void cetak(R&& r) {
    bool pertama = true;
    for (auto&& x : r) {
        if (!pertama) std::cout << ' ';
        std::cout << x;
        pertama = false;
    }
    std::cout << '\n';
}

int main() {
    std::vector<int> transaksi = {120, 45, 300, 85, 500, 60, 220};

    // 1. Pipeline views: filter -> transform -> take, tanpa container baru
    auto promo = transaksi
        | std::views::filter([](int x) { return x >= 100; })
        | std::views::transform([](int x) { return x / 10; })
        | std::views::take(3);
    std::cout << "Pipeline       : ";
    cetak(promo);

    // 2. Lazy evaluation: deret TAK HINGGA tetap aman diiterasi
    std::cout << "5 prima pertama: ";
    cetak(std::views::iota(1) | std::views::filter(prima)
                              | std::views::take(5));

    // 3. drop dan reverse
    std::cout << "drop+reverse   : ";
    cetak(std::views::iota(1, 8) | std::views::drop(2) | std::views::reverse);

    // 4. Algoritma pada ranges: tanpa begin()/end() eksplisit
    std::ranges::sort(transaksi);
    std::cout << "ranges::sort   : ";
    cetak(transaksi);
    return 0;
}

Output yang Dihasilkan:

text
Pipeline       : 12 30 50
5 prima pertama: 2 3 5 7 11
drop+reverse   : 7 6 5 4 3
ranges::sort   : 45 60 85 120 220 300 500

Bedah baris demi baris:

  1. Baris 14-25 (helper cetak generik): berbeda dari chapter-chapter sebelumnya yang menerima const std::vector<int>&, helper ini menerima R&& — referensi ke tipe rentang apa pun — sehingga bisa dipakai untuk vector maupun view. Alasannya: banyak view C++20 tidak dapat diiterasi lewat referensi const (misalnya filter_view yang menyimpan cache iterator internal), sehingga parameter const R& akan menolak pipeline seperti di atas. Pemisah spasi ditaruh sebagai prefix agar tidak ada spasi tersisa di akhir baris output.
  2. Baris 31-34 (membentuk pipeline): tiga adaptor dirangkai dengan | ke vector transaksi. Pada tahap ini belum ada angka yang diproses — promo hanya menyimpan referensi ke transaksi plus daftar adaptor. Tipe lengkapnya panjang, dan itulah alasan auto hampir selalu dipakai untuk variabel view.
  3. Baris 35-36 (iterasi memicu komputasi): range-for di dalam cetak menarik elemen satu per satu. Filter melewatkan 120, 300, 500, 220, transform membaginya 10 menjadi 12, 30, 50, 22, dan take(3) menghentikan penarikan setelah tiga hasil terkumpul — itulah isi baris output pertama.
  4. Baris 40-41 (deret tak hingga): std::views::iota(1) menghasilkan 1, 2, 3, … tanpa batas. Dirangkai dengan filter(prima) dan take(5), program berhenti rapi di 2 3 5 7 11 — bukti nyata lazy evaluation yang kita bedah di bagian berikutnya.
  5. Baris 45 (drop dan reverse): std::views::iota(1, 8) menghasilkan 1 sampai 7, drop(2) membuang dua elemen pertama menjadi 3–7, lalu reverse membalik urutannya menjadi 7 6 5 4 3. Perhatikan reverse dipakai tanpa tanda kurung karena adaptor ini tidak membutuhkan argumen.
  6. Baris 48 (std::ranges::sort(transaksi)): algoritma versi ranges mengurutkan container langsung tanpa begin()/end(). Perbedaannya dengan versi klasik kita bahas di bagian selanjutnya.

Lazy Evaluation: Dihitung Saat Diiterasi #

Baris auto promo = transaksi | std::views::filter(...) | …; di atas tidak memproses satu angka pun. Variabel promo hanyalah objek kecil yang menyimpan referensi ke transaksi plus daftar adaptor — menyalin view semurah menyalin pointer. Komputasi baru terjadi ketika cetak(promo) menjalankan range-for: iterator view menarik elemen dari sumber satu per satu, dan setiap elemen mengalir melewati seluruh rantai adaptor sebelum elemen berikutnya ditarik.

Diagram di atas menggambarkan pipeline pertama. Kotak filter, transform, dan take bukan wadah penyimpan — masing-masing hanya aturan yang diterapkan saat elemen ditarik. Ketika range-for meminta satu elemen, permintaan itu merambat dari kanan ke kiri: take(3) meminta elemen berikutnya dari transform, transform meminta dari filter, dan filter mengambil dari vector sumber sampai menemukan yang lolos. Tidak pernah ada vector antara yang terbentuk di sepanjang proses.

Bukti paling meyakinkan ada di pipeline kedua. std::views::iota(1) menghasilkan deret 1, 2, 3, … tanpa akhir; dengan strategi eager — menyalin dulu, memproses kemudian — program tersebut tidak akan pernah selesai. Karena view menghitung elemen hanya saat diminta, take(5) cukup berhenti setelah lima prima pertama ditemukan, dan program berakhir normal dengan output 2 3 5 7 11.

Lazy evaluation memberi dua keuntungan praktis. Pertama, tidak ada container sementara antar tahap: pipeline empat adaptor tidak membuat satu vector baru pun. Kedua, pekerjaan ikut hemat, karena pada pipeline pertama lambda transform hanya dieksekusi untuk transaksi bernilai minimal 100, bukan untuk seluruh data.

cpp Contoh view menggantung (JANGAN ditiru)
std::ranges::filter_view dataPromo() {
    std::vector<int> transaksi = {120, 45, 300};
    return transaksi | std::views::filter([](int x) { return x >= 100; });
    // transaksi hilang di sini; view yang dikembalikan menunjuk data
    // yang sudah tidak ada -> undefined behavior saat diiterasi
}

Perbedaan Algoritma Ranges dan Klasik #

Sejak C++20, hampir semua algoritma di <algorithm> punya kembaran di namespace std::ranges yang menerima range langsung: std::ranges::sort(v), std::ranges::find(v, nilai), std::ranges::count_if(v, predikat), dan seterusnya. Bentuk ini menggantikan pasangan begin()/end() yang sebelumnya wajib ditulis. Algoritma ranges juga constrained: lewat C++20 concepts, compiler memeriksa sejak awal apakah iterator sumber memenuhi syarat algoritma, sehingga kesalahan terdeteksi lebih cepat dengan pesan error yang lebih jelas. Fitur tambahan yang paling terasa manfaatnya adalah projection — parameter opsional yang memetakan elemen ke kunci operasi.

Program berikut menghadapkan kedua gaya pada kasus yang sama, lalu menutup dengan materialisasi hasil pipeline ke container:

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

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

int main() {
    std::vector<int> angka = {5, 2, 8, 1, 9, 3};

    // Gaya klasik: pasangan iterator begin()/end() ditulis eksplisit
    auto it = std::find(angka.begin(), angka.end(), 8);
    std::cout << "Klasik : indeks " << (it - angka.begin()) << "\n";

    // Gaya ranges: container dikirim langsung tanpa begin()/end()
    auto pos = std::ranges::find(angka, 8);
    std::cout << "Ranges : indeks " << (pos - angka.begin()) << "\n";

    // Projection: urutkan berdasarkan member tanpa lambda tambahan
    std::vector<Produk> katalog = {{"Pena", 3000}, {"Buku", 12000},
                                   {"Penghapus", 2000}};
    std::ranges::sort(katalog, {}, &Produk::harga);
    for (std::size_t i = 0; i < katalog.size(); ++i) {
        if (i > 0) std::cout << ' ';
        std::cout << katalog[i].nama << '=' << katalog[i].harga;
    }
    std::cout << "\n";

    // Materialisasi: hasil pipeline disimpan ke container sungguhan
    std::vector<int> kuadratGenap;
    std::ranges::copy(angka | std::views::filter([](int x) { return x % 2 == 0; })
                             | std::views::transform([](int x) { return x * x; }),
                      std::back_inserter(kuadratGenap));
    for (std::size_t i = 0; i < kuadratGenap.size(); ++i) {
        if (i > 0) std::cout << ' ';
        std::cout << kuadratGenap[i];
    }
    std::cout << "\n";
    return 0;
}

Output yang Dihasilkan:

text
Klasik : indeks 2
Ranges : indeks 2
Penghapus=2000 Pena=3000 Buku=12000
4 64

Bedah baris penting:

  1. Baris 16-18 dan 20-22 (dua gaya find): versi klasik menulis pasangan angka.begin(), angka.end(), sedangkan versi ranges cukup std::ranges::find(angka, 8). Keduanya mengembalikan iterator dengan makna yang sama — menunjuk elemen di indeks 2 — sehingga cara menghitung indeksnya pun identik. Bila suatu saat kita butuh rentang parsial, overload ranges yang menerima iterator (std::ranges::find(angka.begin(), angka.end(), 8)) juga tersedia.
  2. Baris 27 (std::ranges::sort(katalog, {}, &Produk::harga)): argumen kosong {} berarti comparator default (<), dan parameter ketiga adalah projection — alamat member harga dipakai sebagai kunci pengurutan tanpa lambda tambahan. Hasilnya katalog terurut menaik berdasarkan harga: Penghapus=2000 Pena=3000 Buku=12000.
  3. Baris 35-38 (materialisasi): pipeline filter-genap lalu transform-kuadrat dijadikan argumen pertama std::ranges::copy, dengan std::back_inserter sebagai tujuan. Di sinilah view yang sifatnya sementara berubah menjadi vector sungguhan bernama kuadratGenap yang berisi 4 64.

Gaya klasik tetap wajib dikenali. Mayoritas kode C++ yang beredar ditulis sebelum C++20, dan pola [first, last) masih paling jelas untuk memproses bagian tengah container seperti contoh sort tiga elemen pada Chapter Algoritma Berbasis Rentang. Sumber yang bukan range penuh — misalnya iterator yang ditulis manual ke buffer — juga tetap ditangani dengan gaya iterator.

Terakhir, ingat bahwa hasil pipeline berupa view bukanlah wadah. Untuk menyimpannya, gunakan std::ranges::copy dengan std::back_inserter seperti pada program di atas, atau — sejak C++23 — pintasan std::ranges::to<std::vector>() yang membentuk container langsung dari pipeline. Setelah dimaterialisasi, hasilnya berdiri sendiri dan bebas dari risiko view menggantung yang disebut di bagian sebelumnya.

Penutup Section: dari Iterator ke Ranges #

Section Iterators dan Algorithms kini tuntas. Kita mulai dari begin()/end() dan kategori iterator, lalu memakai pola [first, last) pada seluruh algoritma inti: sort, find, binary search, min/max, count, copy/move, transform, idiom remove-erase, partition, accumulate, dan algoritma <numeric>, hingga custom comparator dan function objects. Chapter ini menutup lingkaran tersebut: Ranges dan views bukan pengganti fondasi itu, melainkan lapisan modern di atasnya — boilerplate iterator disembunyikan, sedangkan model iterasi dan concept iterator tetap sama persis di balik layar.

Selama 18 chapter, data yang kita olah selalu sudah berada di memori — vector yang kita isi sendiri lewat initializer list. Program sungguhan membaca data dari luar: ketikan pengguna, file konfigurasi, log, gambar, atau jaringan. Section berikutnya, Input-Output dan File, dibuka dengan Chapter Peta iostream yang membedah keluarga stream di balik std::cout dan std::cin yang selama ini kita pakai tanpa banyak pertanyaan. Setelah itu kita akan memformat output dengan manipulator, membaca dan menulis file teks maupun biner, sampai menjelajah direktori dengan <filesystem>.

Rujukan resmi: Ranges library , std::ranges::filter_view , dan std::ranges::iota_view .