Range dan view (C++20)

Pipeline yang dapat disusun atas container dengan std::views.

Daftar isi

Topik ini membahas pustaka ranges dari C++20, yaitu cara memandang container sebagai rentang (range) yang utuh dan menyusun pipeline transformasi di atasnya dengan std::views. Pembahasan mencakup rentang sebagai satu objek, penyusunan pipeline dengan filter dan transform, pembangkit rentang dengan views::iota, dan materialisasi hasil ke container. Seluruh contoh memerlukan opsi -std=c++23 pada perintah kompilasinya.

Dari pasangan iterator ke rentang#

Algoritma klasik menerima pasangan iterator yang dikirim terpisah, sehingga kesalahan pencampuran pasangan dari container yang berbeda dapat lolos dari pandangan sekilas. C++20 memperkenalkan konsep rentang, yaitu apa pun yang menyediakan begin() dan end() sebagai satu kesatuan. Versi algoritma di namespace std::ranges menerimanya secara langsung, sehingga std::ranges::sort(nilai) menggantikan pemanggilan std::sort(nilai.begin(), nilai.end()) yang lebih panjang.

Di samping algoritma, pustaka ranges menyediakan view, yaitu adapter yang menampilkan data container asalnya dalam bentuk lain tanpa memiliki data itu dan tanpa menyalinnya. View diproses dengan malas (lazy), artinya elemen hanya dihitung ketika sesuatu membacanya, bukan ketika pipelinenya didefinisikan. Karena sifat itu, penyusunan beberapa view sekaligus tidak membangun container perantara apa pun.

Objek view juga ringan dan murah disalin, karena isinya hanya penunjuk beserta metadata kecil. Pipeline yang sudah tersusun dapat disimpan pada variabel auto, dipakai ulang beberapa kali, dan tetap tidak menyalin data sama sekali. Biaya sesungguhnya baru muncul ketika elemen-elemennya dibaca, sesuai sifat malas yang menjadi ciri pustaka ini.

Menyusun pipeline dengan filter dan transform#

View disambungkan dengan operator |, sehingga data mengalir dari kiri ke kanan sebagaimana dibaca. std::views::filter menyaring elemen yang memenuhi kriteria, sedangkan std::views::transform mengubah setiap elemen yang lolos. Keduanya mengembalikan view lagi, sehingga sambungannya dapat diperpanjang sesuai kebutuhan tanpa mengubah pola penulisannya.

cpp pipeline-nilai.cpp
// tidak-dijalankan (butuh -std=c++23; perintah build ada di bawah)
#include <iostream>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> nilai = {70, 85, 55, 90, 60, 75};

    auto lolosAmbang = nilai
        | std::views::filter([](int n) { return n >= 70; })
        | std::views::transform([](int n) { return n + 5; });

    for (int n : lolosAmbang) {
        std::cout << n << '\n';
    }
    return 0;
}
bash
# kompilasi dengan standar C++23
g++ -std=c++23 pipeline-nilai.cpp -o pipeline-nilai
# jalankan program
./pipeline-nilai
text Keluaran
75
90
95
80

Pipeline di atas mengambil nilai ujian yang mencapai 70 atau lebih, lalu menambahkan kurva lima poin pada masing-masing. Tidak ada vector perantara yang dibuat, karena setiap elemen mengalir dari filter ke transform satu demi satu ketika perulangan menariknya. Sebagai pembanding, algoritma klasik menuntut dua putaran terpisah atau std::copy_if dengan container tujuan yang sudah siap.

View yang tersimpan pada lolosAmbang tetap hidup selama nilai ada, dan pembacaannya dapat diulang. Apabila isi nilai berubah, hasil pembacaan berikutnya mengikuti keadaan terbaru, karena view hanya jendela pengamatan dan bukan cuplikan data. Perubahan struktur seperti penghapusan elemen tetap terlarang di tengah pembacaan, dengan alasan yang sama seperti pada invalidasi iterator.

Pembangkit rentang dengan views::iota#

std::views::iota menghasilkan rentang bilangan berurutan tanpa container sama sekali. Bentuknya dengan dua argumen menghasilkan deret dari nilai awal sampai sebelum nilai akhir, sesuai konvensi rentang setengah terbuka. Pembangkit ini menggantikan perulangan penghitung yang biasanya ditulis manual, dan hasilnya dapat langsung disambungkan ke view lain.

cpp kuadrat-genap.cpp
// tidak-dijalankan (butuh -std=c++23; perintah build ada di bawah)
#include <iostream>
#include <ranges>

int main() {
    auto kuadratGenap = std::views::iota(1, 11)
        | std::views::filter([](int n) { return n % 2 == 0; })
        | std::views::transform([](int n) { return n * n; });

    for (int n : kuadratGenap) {
        std::cout << n << ' ';
    }
    std::cout << '\n';
    return 0;
}
bash
# kompilasi dengan standar C++23
g++ -std=c++23 kuadrat-genap.cpp -o kuadrat-genap
# jalankan program
./kuadrat-genap
text Keluaran
4 16 36 64 100 

Rentang dari 1 sampai sebelum 11 disaring sampai tersisa bilangan genap, lalu setiap sisa dikuadratkan. Keluaran diakhiri satu spasi karena setiap angka dicetak dengan pemisah spasi. Rantai pada contoh tidak menyentuh container apa pun, sehingga seluruh pipeline berjalan tanpa satu alokasi pun, dan pola ini cocok untuk deret indeks serta perhitungan yang sifatnya matematis.

Bentuk std::views::iota dengan satu argumen menghasilkan deret tanpa batas. Deret tersebut tetap aman dipakai bersama filter karena elemennya baru dihitung saat dibaca, tetapi perulangan atasnya wajib dibatasi oleh view lain seperti std::views::take. Pustaka ini juga menyediakan adapter lain yang lazim dipakai, yaitu views::take yang mengambil n elemen pertama, views::drop yang melempar n elemen pertama, dan views::reverse yang membalik urutan penyusuran.

Materialisasi hasil ke container#

View tidak menyimpan hasil apa pun, sehingga hasil pipeline yang perlu dipakai berulang kali harus disalin ke container yang memiliki datanya sendiri. Proses ini disebut materialisasi. Cara standarnya adalah std::ranges::copy dari view ke vector kosong yang dibungkus std::back_inserter, mirip kebiasaan algoritma klasik.

cpp materialisasi-sensor.cpp
// tidak-dijalankan (butuh -std=c++23; perintah build ada di bawah)
#include <algorithm>
#include <iostream>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> suhu = {28, 35, 31, 26, 33};

    auto diAtasAmbang = suhu
        | std::views::filter([](int n) { return n > 30; });

    // view disalin nilainya ke vector yang memiliki datanya sendiri
    std::vector<int> rekap;
    std::ranges::copy(diAtasAmbang, std::back_inserter(rekap));

    std::cout << "Jumlah tercatat: " << rekap.size() << '\n';
    for (int n : rekap) {
        std::cout << n << '\n';
    }
    return 0;
}
bash
# kompilasi dengan standar C++23
g++ -std=c++23 materialisasi-sensor.cpp -o materialisasi-sensor
# jalankan program
./materialisasi-sensor
text Keluaran
Jumlah tercatat: 3
35
31
33

Hasil penyaringan suhu di atas ambang 30 kini tersimpan pada rekap dan dapat dibaca berkali-kali tanpa menjalankan pipeline lagi. C++23 menyediakan pintasan berupa std::ranges::to, sehingga sambungan suhu | std::views::filter(...) | std::ranges::to<std::vector>() langsung menghasilkan vector tanpa std::ranges::copy.

Materialisasi juga menjadi titik keputusan performa. Pipeline yang hanya dibaca sekali sebaiknya dibiarkan malas agar tidak ada penyalinan sama sekali, sedangkan hasil yang dipakai berulang kali sebaiknya dimaterialisasi agar perhitungannya tidak diulang setiap pembacaan.

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul ketika pustaka ranges dipakai.

  1. Mengompilasi tanpa -std=c++23. Header <ranges> belum tersedia pada standar default g++ 14.2, sehingga muncul error ranges is not a member of std. Tambahkan opsi standarnya pada perintah build.
  2. Menganggap view menyimpan data. Pemanggilan size() pada hasil filter misalnya tidak tersedia, karena view tidak menjanjikan ukuran sebelum disusuri. Materialisasikan lebih dulu bila ukurannya perlu diketahui.
  3. Mengubah container sumber sementara viewnya sedang dipakai. View memakai iterator di balik layar, sehingga aturan invalidasi iterator tetap berlaku. Tunda perubahan struktur sampai pipeline selesai dibaca.

Ringkasan#

  • Rentang menyatukan begin() dan end() sebagai satu objek, dan namespace std::ranges menyediakan versi algoritma yang menerimanya langsung.
  • View seperti filter dan transform disambung dengan operator |, diproses dengan malas, dan tidak membangun container perantara.
  • std::views::iota membangkitkan deret angka, sedangkan materialisasi dengan std::ranges::copy atau std::ranges::to menyimpan hasil pipeline ke container milik sendiri.

Referensi#