Chapter range-based for di section Control Flow sudah memperkenalkan bentuk dasarnya: for (int x : koleksi) mengunjungi setiap elemen tanpa indeks, dan kita tahu perbedaan auto, auto&, serta const auto& pada deklarasinya. Chapter ini membuka kap mesinnya: kita lihat pernyataan yang sebenarnya dibangun compiler di belakang loop ini — pengetahuan yang menjelaskan mengapa ada koleksi yang bisa di-loop dan ada yang tidak, serta mengapa ada kombinasi temporary yang berujung pada undefined behavior. Setelah itu kita bahas tiga hal yang tidak tersentuh chapter dasar: init-statement C++20 yang menambahkan deklarasi awal ke dalam loop, perangkap umur temporary yang masih aktif sampai hari ini, dan cara membuat class buatan sendiri ikut didukung range-based for hanya dengan menyediakan begin() dan end().

Apa yang Compiler Bangun di Belakang Layar #

Compiler tidak mengenal loop yang bekerja dengan sendirinya — range-based for adalah gula sintaksis yang diperluas menjadi kode biasa berikut:

cpp Perluasan konseptual yang dibangun compiler
{
    auto&& __range = <ekspresi di titik dua>;   // 1. ikat sumber
    auto __begin = begin(__range);              // 2. ambil titik awal
    auto __end   = end(__range);                // 3. ambil batas akhir
    for (; __begin != __end; ++__begin) {       // 4. selama belum sama
        <deklarasi elemen> = *__begin;          // 5. baca elemen
        // isi badan loop
    }
}

Baris pertama adalah kuncinya: ekspresi di titik dua diikat ke sebuah reference auto&&, dan umur hidup objek yang diikat ke reference itu diperpanjang sampai loop selesai. Itulah sebabnya for (int x : buatData()) aman — temporary yang dikembalikan buatData() tetap hidup selama loop berjalan. Tetapi perhatikan kata “objek yang diikat”: hanya ekspresi paling luar yang dijamin, sedangkan temporary yang tercipta di tengah ekspresi bisa berakhir lebih cepat. Teks __begin, __end, dan __range adalah nama ilustratif; yang penting kita paham lima langkahnya, karena setiap aturan pada bab ini kembali ke baris pertama perluasan ini.

Program Pendalaman #

cpp
// range-for.cpp — pendalaman range-based for: init-statement, temporary, dan class sendiri
// Kompilasi: g++ -std=c++23 -Wall -Wextra range-for.cpp -o range-for

#include <print>
#include <span>
#include <string>
#include <vector>

// fungsi yang MENGEMBALIKAN collection by value (sumber temporary)
std::vector<int> buatData() {
    return {10, 20, 30, 40};
}

// struct pemilik data + method yang mengembalikan VIEW ke data membernya
struct Buffer {
    std::vector<int> isi{1, 2, 3, 4, 5};

    std::span<int> potongan() { return std::span<int>(isi).first(3); }
};

// class sendiri yang bisa di-loop: cukup sediakan begin() dan end()
class RentangAngka {
    int awal_;
    int akhir_;   // eksklusif
public:
    RentangAngka(int awal, int akhir) : awal_(awal), akhir_(akhir) {}

    struct Iterator {          // iterator ringan: pembungkus posisi
        int posisi;

        int operator*() const { return posisi; }
        Iterator& operator++() { ++posisi; return *this; }
        bool operator==(const Iterator& lain) const { return posisi == lain.posisi; }
        bool operator!=(const Iterator& lain) const { return posisi != lain.posisi; }
    };

    Iterator begin() const { return Iterator{awal_}; }
    Iterator end() const { return Iterator{akhir_}; }
};

int main() {
    std::print("-- init-statement (C++20)\n");
    for (auto data = buatData(); auto x : data) std::print("{} ", x);
    std::print("\n");
    // std::print("{}", data.size());  // error: 'data' hanya hidup di dalam loop

    std::print("-- temporary langsung sebagai range (aman)\n");
    for (int x : buatData()) std::print("{} ", x);
    std::print("\n");

    std::print("-- init-statement menjaga pemilik data tetap hidup\n");
    for (auto buf = Buffer{}; int x : buf.potongan()) std::print("{} ", x);
    std::print("\n");

    std::print("-- auto& mengubah, const auto& membaca\n");
    std::vector<int> nilai{1, 2, 3};
    for (auto& x : nilai) x *= 10;
    for (const auto& x : nilai) std::print("{} ", x);
    std::print("\n");

    std::print("-- class sendiri dengan begin()/end()\n");
    for (int x : RentangAngka(1, 6)) std::print("{} ", x);
    std::print("\n");
    return 0;
}

Output yang Dihasilkan:

text
-- init-statement (C++20)
10 20 30 40 
-- temporary langsung sebagai range (aman)
10 20 30 40 
-- init-statement menjaga pemilik data tetap hidup
1 2 3 
-- auto& mengubah, const auto& membaca
10 20 30 
-- class sendiri dengan begin()/end()
1 2 3 4 5 

Mari kita bedah:

  1. Baris 43 (init-statement C++20): bagian sebelum titik dua kini bisa berisi deklarasi — for (auto data = buatData(); auto x : data). Variabel data dihitung satu kali, hidup hanya selama loop, dan tidak memenuhi scope fungsi — baris 45 memang error bila dibuka komentarnya. Pola ini menjaga scope tetap rapat: variabel bantu loop tidak bocor keluar.
  2. Baris 48 (temporary langsung): kembalian buatData() diikat utuh ke reference internal loop sehingga hidup sampai loop selesai — keluaran 10 20 30 40 tercetak tanpa selisih.
  3. Baris 52 (init-statement sebagai penjaga pemilik): for (auto buf = Buffer{}; int x : buf.potongan()) memisahkan dua peran — buf adalah pemilik data, sedangkan buf.potongan() hanyalah view yang menunjuk ke data miliknya. Karena buf hidup selama loop, view-nya tidak pernah menggantung. Pola ini adalah solusi idiomatis untuk perangkap yang kita bedah di section berikut.
  4. Baris 57-58 (ubah vs baca): auto& x mengikat elemen asli sehingga x *= 10 mengubah nilai sungguhan; const auto& x membaca tanpa menyalin dan tanpa izin mengubah. Aturan deduksinya persis seperti tabel pada chapter auto dan Type Deduction — by value menyalin, by reference mengalias.
  5. Baris 62 (class sendiri): RentangAngka(1, 6) bukan container standar, tetapi loop menerimanya tanpa kerja tambahan karena class-nya menyediakan begin(), end(), dan iterator yang bisa *, ++, dan !=. Delapan angka dari 1 sampai 5 tercetak sesuai batas eksklusifnya.

Perangkap Umur Temporary #

Baris pertama perluasan (auto&& __range = ...) memperpanjang umur ekspresi paling luar saja. Masalah muncul ketika ekspresi itu bukan pemilik data, melainkan view yang menunjuk ke pemilik lain yang ikut temporary:

cpp Diperangkap: range yang menunjuk data pemilik yang sudah berakhir
struct Buffer {
    std::vector<int> isi{1, 2, 3, 4, 5};

    std::span<int> potongan() { return std::span<int>(isi).first(3); }
};

Buffer buatBuffer() { return Buffer{}; }

for (int x : buatBuffer().potongan()) {   // BAHAYA: pemilik data temporary
    std::print("{}\n", x);
}

Yang diikat oleh loop adalah temporary std::span hasil potongan() — dan span itu hanyalah alamat plus panjang. Objek Buffer yang memuat isi diciptakan di dalam ekspresi dan berakhir di akhir baris yang sama, jadi sejak iterasi pertama loop membaca memori yang sudah dikembalikan. range-based for tidak menyelamatkan temporary yang tersarang seperti ini. g++ 14.2 memberi peringatan eksplisit untuk kode di atas — inilah teks aslinya:

text Peringatan compiler: possibly dangling reference to a temporary
dang.cpp: In function 'int main()':
dang.cpp:14:40: warning: possibly dangling reference to a temporary [-Wdangling-reference]
   14 |     for (int x : buatBuffer().potongan()) {   // BAHAYA: pemilik data temporary
      |                                        ^
dang.cpp:14:39: note: the temporary was destroyed at the end of the full expression 'buatBuffer()().Buffer::potongan()'
   14 |     for (int x : buatBuffer().potongan()) {   // BAHAYA: pemilik data temporary
      |                  ~~~~~~~~~~~~~~~~~~~~~^~

Standar C++23 memperbaiki luka ini lewat proposal P2718R0: umur semua temporary dalam ekspresi range diperpanjang, sehingga kode di atas menjadi aman di compiler yang mengadopsinya. Karena sifatnya perbaikan defect report, perilaku barunya diharapkan diterapkan mundur ke standar lama — tetapi toolchain kita (g++ 14.2) masih menerapkan aturan lama; kami memverifikasi program di atas benar-benar membaca memori mati dengan AddressSanitizer (laporan heap-use-after-free, alamatnya bervariasi antar mesin sehingga tidak dikutip). Status dukungan compiler untuk P2718R0 bisa dilacak di halaman cppreference .

Membuat Class Sendiri Ikut Didukung #

Class RentangAngka pada baris 21-39 memperlihatkan kontrak minimum sebuah range. begin() dan end() mengembalikan objek iterator — di sini struct Iterator ringan yang hanya membungkus satu int posisi. Empat operator itulah yang membuatnya bekerja: operator* menyediakan akses elemen saat *__begin, operator++ menggeser posisi, dan operator==/operator!= dipakai untuk menguji “sudah sampai akhir atau belum”. end() sengaja mengembalikan posisi setelah elemen terakhir (exclusive), sehingga rentang 1..6 mencetak 1 2 3 4 5 dan loop berhenti dengan rapi.

Dua catatan praktis sebelum kita lanjut. Pertama, begin()/end() tidak wajib berupa method — free function begin(obj) juga dikenali, itulah cara std::array C-style yang polos ikut didukung. Kedua, sebelum menulis class range sendiri, cek dulu <ranges>: std::views::iota(1, 6) misalnya sudah menyediakan rentang angka yang persis seperti RentangAngka, siap pakai dan siap dirangkai dengan view lain. Kode custom tetap berharga saat kita punya struktur domain sendiri — hasil query, stream record, atau graph traversal — dan ingin strukturnya ikut ergonomis di setiap loop.

range-based for dan lambda adalah dua teman satu tim: loop mengunjungi elemen, lambda membungkus aksi yang ingin dilakukan pada tiap kunjungan. Chapter berikutnya mendalami sisi lambda — init-capture yang memindahkan data masuk, lambda generik dengan parameter template, dan pola immediately-invoked lambda untuk inisialisasi konstanta.

Rujukan resmi: cppreference — range-based for statement , cppreference — dukungan compiler C++23 .