Teknologi

C++26: while(true); Bukan Lagi Undefined Behaviour, Ini Isi P2809R3

C++26: while(true); Bukan Lagi Undefined Behaviour, Ini Isi P2809R3

Sebuah program C++ berisi while(true); tanpa side effect resmi berhenti menjadi undefined behaviour di C++26. Perubahan ini dibawa oleh paper P2809R3, dan mungkin terdengar seperti formalitas kecil. Faktanya, pola loop kosong persis satu baris itu dipakai luas di kode embedded dan kernel sebagai pola halt-on-error, dan sebelum C++26 kompiler secara legal boleh menghapus loop tersebut, membuat eksekusi jatuh ke kode setelahnya. Postmortem-nya sudah ada, konsekuensinya nyata, dan proposal ini akhirnya diterima sekaligus berstatus defect report, artinya implementasi boleh memperbaikinya bahkan untuk mode bahasa yang lebih lama.

Artikel ini mengurai sejarah aturannya, kenapa C dan C++ selama ini berbeda sikap soal loop yang sama, dan apa saja yang memenuhi syarat sebagai trivial infinite loop versi C++26.

Aturan yang Membuat while(true); Jadi Undefined

Akar masalahnya adalah forward progress guarantee yang diperkenalkan C++11 bersama dukungan threading. Standar di bagian [intro.progress] menyatakan implementasi boleh mengasumsikan setiap thread pada akhirnya akan melakukan salah satu dari: terminasi, memanggil fungsi I/O library, mengakses volatile glvalue, atau melakukan operasi synchronisation dan atomik. Loop while(true); dengan body kosong tidak melakukan satu pun dari daftar itu. Konsekuensinya, eksekusi yang terjebak selamanya di loop semacam itu punya perilaku tak terdefinisi, dan optimizer bebas menyimpulkan path tersebut mustahil dicapai.

Kuasa hukum optimizer dalam situasi ini adalah godbolt, dan contoh yang dipakai Sandor Dargo di blog-nya (16 September 2026) langsung menunjukkan betapa absurd kelihatannya: program main berisi while(true); diikuti satu fungsi unreachable() yang mencetak "Hello world!" di TU yang sama. Saat dikompilasi dengan Clang, program itu ternyata mencetak Hello world!. Loop dihapus, main jatuh keluar, dan fungsi yang "tidak akan pernah tercapai" itu malah dieksekusi. Bukan bug kompiler, murni UB yang dipakai dengan taat.

C11 Selalu Lebih Benar di Sini

Fakta yang bikin banyak orang kaget: bahasa C tidak pernah punya masalah ini. C++11 dan C11 memperkenalkan aturan forward progress di waktu yang hampir berbarengan, tapi C menambahkan satu klausul ekstra, loop dengan controlling expression berupa constant expression tidak boleh diasumsikan akan Terminasi. Jadi while(1); sudah well-defined di C sejak C11. C++ tidak pernah adopsi klausul itu, dan sisanya menjadi satu dari daftar panjang divergensi C-vs-C++ yang harus dijelaskan di interview.

Lalu kenapa ada orang menulis loop kosong abadi? Untuk kode aplikasi, memang jarang. Tapi untuk bare-metal dan kernel, ini pola baku halt-on-error: saat inisialisasi hardware gagal dan tidak ada sistem operasi untuk dieksekusi mundur, jalur terakhirnya adalah berhenti total sambil tetap memegang kendali eksekusi. Post aslinya menyebut pola itu umum di kode embedded: jika inisialisasi gagal, log error, lalu while(true); dan selesai. Masalahnya, "berhenti total" itu justru yang dihapus optimizer, dan perangkat lanjut menjalankan sembarang instruksi setelah handler dalam kondisi korup. Untuk kode security-critical, ini bukan teori.

Definisi Baru: Kapan Sebuah Loop Disebut Trivial

C++26 tidak serta-merta menyalin aturan C yang lebih longgar; opsi itu dipertimbangkan dan ditolak karena dianggap menghambat optimasi yang sah. P2809R3 malah mendefinisikan kategori sempit bernama trivial infinite loop dengan dua syarat kumulatif. Pertama, loop harus berupa trivially empty iteration statement, body-nya benar-benar kosong, hanya ";" atau "{}". Satu statement apa pun di dalamnya, bahkan string literal tanpa efek "a string";, langsung menggugurkan syarat. Kedua, controlling expression harus constant expression yang bernilai true, dan pada for loop tanpa kondisi, true bersifat implisit.

Sehingga, begini hasilnya pada daftar contoh dari post aslinya. while(true); lolos, lolos. for(;;); lolos. do {} while(true); lolos. Variabel constexpr bool go = true; lalu while(go); juga lolos karena go konstan. Yang tidak lolos: while(true) { "a string"; } karena body ada statement; while(true) if (done) break; karena body tidak kosong; while(true) if constexpr (false) break; karena tidak cocok sintaks trivially empty; dan bool done=false; while(!done); karena condition-nya bukan constant expression.

Mekanisme Penggantinya: yield() yang Disisipkan

Bagian yang paling teknis dari perubahan ini bukan definisinya, melainkan apa yang dilakukan kompiler terhadap loop yang lolos syarat. Sesuai P2809R3, body trivial infinite loop diganti dengan panggilan std::this_thread::yield(). Dengan sisipan itu, loop memperoleh kembali semantics forward progress yang sebelumnya tidak pernah ia punya, karena thread sekarang secara periodik relinquish giliran menjalankan. Garansi forward progress itu sendiri ikut diamandemen: salah satu perilaku sah yang boleh diassumsikan dari sebuah thread kini mencakup "melanjutkan eksekusi trivial infinite loop". Praktisnya, optimizer tidak bisa lagi memperlakukan loop tersebut sebagai mustahil dan memotong kontrol flow ke baliknya.

Untuk programir aplikasi, perubahan perilaku runtimes-nya nyaris nol; loop yang tadinya spin penuh jadi yield periodik hanya dalam kasus body kosong yang toh tidak pernah ditulis dengan niat serius. Yang berubah drastis adalah kepastian hukum, dari "kompiler boleh berbuat semaunya" menjadi "loop ini memiliki perilaku yang dispesifikasikan".

Catatan Kritis: Freestanding dan Mode Lama

Dua catatan dari post Sandor Dargo yang penting tidak tenggelam di euforia. Pertama, implementasi freestanding mendapat kelonggaran: terserah implementation-defined apakah penggantian dengan yield() terjadi atau tidak. Untuk bare-metal, ini disengaja, mengubah loop halt menjadi yield kooperatif justru bisa memperkenalkan perilaku yang tidak pernah diniatkan programmer; dia menghendaki berhenti, bukan berbagi giliran dengan scheduler yang bahkan belum tentu ada. Kedua, karena P2809R3 diterima sebagai defect report, implementasi boleh menerapkan perbaikan ini bahkan saat kompilasi dalam mode C++14 atau C++20. Efek sampingnya, contoh klasik "Clang menghapus while(true); dan mencetak Hello world" mungkin sudah tidak bisa direproduksi di kompiler keluaran terbaru, bukan karena bug-nya hilang, tapi karena DR berlaku surut.

Kalau penasaran ingin melihat sendiri, buka compiler explorer, pasang Clang versi lama dengan mode C++17, lalu bandingkan assembly-nya dengan C++26; perbedaan hilangnya blok loop itu persis demonstrasi yang membuat topik ini layak jadi bahan obrolan ruangan, bukan cuma catatan kaki standar.

Dampak Praktis: Kode Embedded dan Audit Static Analyzer

Di luar teori, ada tiga konsekuensi yang bisa dirasakan besok pagi. Untuk tim embedded, pola halt-on-error yang selama ini hidup dalam zona abu-abu konformitas kini legal secara eksplisit, dan hasil build dengan kompiler berbeda menjadi lebih bisa dipercaya seragam. Untuk reviewer kode, loop kosong tidak lagi memicu alarm "pasti bug", setidaknya tidak berdasarkan standar. Untuk pembuat static analyzer dan tool sanitasi, aturan baru ini berarti update model semantik; tool yang masih memodelkan while(true); sebagai UB akan memberi false positive terhadap kode yang sekarang sah. Kelas perubahan seperti ini memang terdengar kecil, tapi kategori "UB yang dilindungi jadi defined" persis yang mengurangi jarak antara kode yang lolos kompile dan kode yang perilakunya terprediksi saat di-flash ke perangkat.

Kenapa Kompiler Menghapus, Bukan Sekadar Membiarkan

Penting dipahami bahwa penghapusan loop itu bukan keputusan iseng. Setelah optimizer memegang izin bahwa thread tidak mungkin selamanya terjebak di loop tanpa efek, seluruh kode setelah loop secara otomatis berstatus unreachable dari jalur tersebut, dan dead code elimination bekerja seperti biasa: basic block yang tidak pernah dieksekusi boleh dibuang. Masalah muncul ketika ada fungsi yang secara fisik diletakkan setelah loop, seperti contoh godbolt di atas, sehingga pembuangan itu justru mengubah output yang terlihat. Di C++ pra-26 tidak ada yang salah dengan itu, karena eksekusi yang tidak pernah keluar dari loop adalah UB, dan UB memang membuat apa pun sah. Di C++26 pintu itu ditutup untuk kategori loop trivial, dan optimizer harus memperlakukannya sebagai state hidup.

Pertanyaan yang Muncul Segera

Bagian ini menjawab tiga keberatan yang hampir pasti muncul di thread review kode. Pertama, bukankah loop kosong itu memang bug? Sering iya, dan tool seperti warnings kompilator tetap berhak memberitakannya; yang berubah hanya status standarnya, dari undefined menjadi defined. Defined bukan berarti bermakna, sama seperti variabel yang dibaca lalu dibuang juga defined tapi tidak berguna. Kedua, apakah ini membuka pintu untuk busy-wait spinlock tanpa yield yang sekarang jadi legal? Tidak selepas yang dikira: loop spin yang memuat variabel non-atomic tetap tidak termasuk trivial infinite loop karena controlling expression-nya bukan constant expression, jadi aturan lama tetap bekerja di sana. Ketiga, apakah program yang sebelumnya berhasil karena loop dihapus kini berubah hasil? Ya, dan itu justru intinya; program semacam itu tidak pernah punya hasil yang dispesifikasikan, yang berubah sekarang adalah siapa yang boleh memutuskan apa yang terjadi, kompiler atau programmer.

Satu hal lagi yang layak disadari: perubahan ini hidup di level bahasa, bukan di tooling. Kode yang sama tetap butuh kompiler terbaru dengan mode C++26 aktif untuk mendapat garansinya, dan tim yang terikat toolchain lama (sering terjadi di industri yang bersertifikasi) boleh jadi baru melihat efeknya bertahun-tahun kemudian. Tidak heran post aslinya mengemas berita ini sebagai bagian dari seri panjang pengurangan UB di C++26, tempat setiap proposal kecil menutup satu celah spesifik, bukan revolusi sekaligus.

Penutup: Detail Kecil, Nilai Didaktis Besar

Kasus trivial infinite loop adalah ilustrasi paling bersih kenapa forward progress guarantee ada, dan sekaligus kenapa ia sering disalahpahami. Garansi itu bukan janji bahwa program akan selesai, melainkan izin bagi implementasi untuk mengasumsikan thread tidak macet di tempat tanpa efek teramati. Begitu loop tidak punya efek teramati, asumsi itu jadi senjata makan tuan. C++26 menambal titik buta ini dengan kategori sempit dan mekanisme yield(), membiarkan sisanya seperti dulu. Untuk yang ingin menelusuri keluarga perubahan UB lainnya di standar baru ini, artikel hardening standard library di C++26 di Toolkuy membahas sisi library dari cerita yang sama, dan seri tutorial C++ modern lain bisa dicari lewat indeks blog. Sementara untuk sumber utama pembahasan ini, post Sandor Dargo di 16 September 2026 memuat tabel lolos-gugur lengkap plus tautan godbolt untuk bereksperimen langsung, sekaligus pengingat bahwa standar bahasa yang matang dibangun dari detail-detail sekecil ini.

💬 Komentar (0)

Belum ada komentar. Jadilah yang pertama! 💬

Komentar akan muncul setelah moderasi.