Urutan evaluasi indeks array (versus ekspresi) di C


47

Melihat kode ini:

static int global_var = 0;

int update_three(int val)
{
    global_var = val;
    return 3;
}

int main()
{
    int arr[5];
    arr[global_var] = update_three(2);
}

Entri array mana yang diperbarui? 0 atau 2?

Apakah ada bagian dalam spesifikasi C yang menunjukkan prioritas operasi dalam kasus khusus ini?


21
Ini bau perilaku yang tidak terdefinisi. Ini tentu saja sesuatu yang seharusnya tidak pernah dikodekan secara sengaja.
Fiddling Bits

1
Saya setuju itu adalah contoh dari pengkodean yang buruk.
Jiminion

4
Beberapa hasil anekdotal: godbolt.org/z/hM2Jo2
Bob__

15
Ini tidak ada hubungannya dengan indeks array atau urutan operasi. Ini ada hubungannya dengan apa yang spesifikasi C sebut "titik sekuens", dan khususnya, fakta bahwa ekspresi penetapan TIDAK membuat titik sekuens antara ekspresi kiri dan kanan, sehingga kompiler bebas untuk melakukan apa adanya memilih.
Lee Daniel Crocker

4
Anda harus melaporkan permintaan fitur ke clangsehingga kode ini memicu IMHO peringatan.
malat

Jawaban:


51

Orde Operan Kiri dan Kanan

Untuk melakukan penugasan dalam arr[global_var] = update_three(2), implementasi C harus mengevaluasi operan dan, sebagai efek samping, memperbarui nilai yang disimpan dari operan kiri. C 2018 6.5.16 (yaitu tentang penugasan) paragraf 3 memberitahu kita tidak ada urutan di operan kiri dan kanan:

Evaluasi operan tidak dilakukan.

Ini berarti implementasi C bebas untuk menghitung lvalue arr[global_var] terlebih dahulu (dengan menghitung lvalue, kami bermaksud mencari tahu apa yang dimaksud dengan ungkapan ini), kemudian mengevaluasi update_three(2), dan akhirnya menetapkan nilai yang terakhir ke yang sebelumnya; atau untuk mengevaluasi update_three(2)dulu, lalu menghitung nilai, lalu menugaskan yang pertama ke yang terakhir; atau untuk mengevaluasi nilai dan update_three(2)dalam beberapa cara yang dicampur dan kemudian menetapkan nilai yang tepat untuk nilai kiri.

Dalam semua kasus, penugasan nilai ke nilai harus datang terakhir, karena 6.5.16 3 juga mengatakan:

... Efek samping dari memperbarui nilai yang disimpan dari operan kiri diurutkan setelah perhitungan nilai operan kiri dan kanan ...

Pelanggaran Sequencing

Beberapa mungkin merenungkan tentang perilaku tidak terdefinisi karena menggunakan global_vardan memperbaruinya secara terpisah yang melanggar 6.5 2, yang mengatakan:

Jika efek samping pada objek skalar tidak diikuti relatif terhadap efek samping yang berbeda pada objek skalar yang sama atau perhitungan nilai menggunakan nilai objek skalar yang sama, perilaku tidak terdefinisi ...

Cukup umum bagi banyak praktisi C bahwa perilaku ekspresi seperti x + x++tidak didefinisikan oleh standar C karena mereka berdua menggunakan nilai xdan secara terpisah memodifikasinya dalam ekspresi yang sama tanpa diurutkan. Namun, dalam hal ini, kami memiliki panggilan fungsi, yang menyediakan beberapa urutan. global_vardigunakan arr[global_var]dan diperbarui dalam pemanggilan fungsi update_three(2).

6.5.2.2 10 memberitahu kita ada titik sekuens sebelum fungsi dipanggil:

Ada titik urutan setelah evaluasi penunjuk fungsi dan argumen aktual tetapi sebelum panggilan yang sebenarnya ...

Di dalam fungsi, global_var = val;adalah ekspresi penuh , dan begitu juga 3di return 3;, per 6.8 4:

Sebuah ekspresi penuh adalah ekspresi yang bukan merupakan bagian dari ungkapan lain, atau bagian dari deklarator atau deklarator abstrak ...

Lalu ada titik berurutan antara dua ekspresi ini, sekali lagi per 6,8 4:

... Ada titik urutan antara evaluasi ekspresi penuh dan evaluasi ekspresi penuh berikutnya untuk dievaluasi.

Dengan demikian, implementasi C dapat mengevaluasi arr[global_var]pertama dan kemudian melakukan pemanggilan fungsi, dalam hal ini ada titik urutan di antara mereka karena ada satu sebelum pemanggilan fungsi, atau dapat mengevaluasi global_var = val;dalam pemanggilan fungsi dan kemudian arr[global_var], dalam hal ini ada titik urutan di antara mereka karena ada satu setelah ekspresi penuh. Jadi perilakunya tidak ditentukan — salah satu dari kedua hal itu dapat dievaluasi terlebih dahulu — tetapi tidak terdefinisi.


24

Hasil di sini tidak ditentukan .

Sementara urutan operasi dalam ekspresi, yang menentukan bagaimana subekspresi dikelompokkan, didefinisikan dengan baik, urutan evaluasi tidak ditentukan. Dalam hal ini berarti global_vardapat dibaca terlebih dahulu atau ajakan untuk update_threeterjadi lebih dulu, tetapi tidak ada cara untuk mengetahui yang mana.

Tidak ada perilaku tidak terdefinisi di sini karena panggilan fungsi memperkenalkan titik urutan, seperti halnya setiap pernyataan dalam fungsi termasuk yang memodifikasi global_var.

Untuk memperjelas, standar C mendefinisikan perilaku yang tidak terdefinisi dalam bagian 3.4.3 sebagai:

perilaku yang tidak terdefinisi

perilaku, setelah menggunakan konstruksi program yang tidak dapat diakses atau salah atau data yang salah, yang untuknya Standar Internasional ini tidak mengharuskan persyaratan

dan mendefinisikan perilaku yang tidak ditentukan dalam bagian 3.4.4 sebagai:

perilaku yang tidak ditentukan

penggunaan nilai yang tidak ditentukan, atau perilaku lain di mana Standar Internasional ini menyediakan dua atau lebih kemungkinan dan tidak memaksakan persyaratan lebih lanjut yang dipilih dalam hal apa pun

Standar menyatakan bahwa urutan evaluasi argumen fungsi tidak ditentukan, yang dalam hal ini berarti bahwa arr[0]akan diatur ke 3 atau arr[2]diatur ke 3.


"Panggilan fungsi memperkenalkan titik urutan" tidak cukup. Jika operan kiri dievaluasi terlebih dahulu, itu sudah mencukupi, sejak itu titik sekuens memisahkan operan kiri dari evaluasi dalam fungsi. Tetapi, jika operan kiri dievaluasi setelah pemanggilan fungsi, titik urutan karena pemanggilan fungsi tidak antara evaluasi dalam fungsi dan evaluasi operan kiri. Anda juga memerlukan titik urutan yang memisahkan ekspresi penuh.
Eric Postpischil

2
@EricPostpischil Dalam terminologi pra-C11 ada titik urutan pada masuk dan keluar dari suatu fungsi. Dalam terminologi C11 seluruh fungsi tubuh secara tidak pasti diurutkan sehubungan dengan konteks panggilan. Keduanya menetapkan hal yang sama, hanya menggunakan istilah yang berbeda
MM

Ini benar-benar salah. Urutan evaluasi argumen penugasan tidak ditentukan. Adapun hasil dari penugasan khusus ini, itu adalah pembuatan array dengan konten yang tidak dapat diandalkan, baik yang tidak dapat dipercaya dan secara intrinsik salah (tidak konsisten dengan semantik atau salah satu dari hasil yang dimaksudkan). Kasus sempurna dari perilaku yang tidak terdefinisi.
kuroi neko

1
@ kuroineko Hanya karena output dapat bervariasi tidak secara otomatis menjadikannya perilaku yang tidak terdefinisi. Standar memiliki definisi yang berbeda untuk perilaku yang tidak ditentukan vs yang tidak ditentukan, dan dalam situasi ini adalah yang terakhir.
dbush

@EricPostpischil Anda memiliki titik sekuens di sini (dari C11 Lampiran C informatif): "Antara evaluasi perancang fungsi dan argumen aktual dalam panggilan fungsi dan panggilan aktual. (6.5.2.2)", "Antara evaluasi ekspresi penuh dan ekspresi penuh berikutnya untuk dievaluasi ... / - / ... ekspresi (opsional) dalam pernyataan pengembalian (6.8.6.4) ". Dan well, di setiap titik koma juga, karena itu ekspresi penuh.
Lundin

1

Saya mencoba dan mendapat entri 0 diperbarui.

Namun menurut pertanyaan ini: akankah sisi kanan ekspresi selalu dievaluasi terlebih dahulu

Urutan evaluasi tidak ditentukan dan tidak dilakukan. Jadi saya pikir kode seperti ini harus dihindari.


Saya mendapat pembaruan di entri 0 juga.
Jiminion

1
Perilaku ini tidak ditentukan tetapi tidak ditentukan. Tergantung pada keduanya secara alami harus dihindari.
Antti Haapala

@AnttiHaapala yang saya edit
Mickael B.

1
Hmm ah dan tidak diurutkan tetapi tidak diurutkan ... 2 orang yang berdiri secara acak dalam antrian secara tidak pasti diurutkan. Neo di dalam Agen Smith tidak diurusi dan perilaku yang tidak terdefinisi akan terjadi.
Antti Haapala

0

Karena tidak masuk akal untuk memancarkan kode untuk suatu tugas sebelum Anda memiliki nilai untuk ditugaskan, kebanyakan kompiler C pertama-tama akan memancarkan kode yang memanggil fungsi dan menyimpan hasilnya di suatu tempat (daftar, susun, dll.), Kemudian mereka akan memancarkan kode yang menulis nilai ini ke tujuan akhirnya dan oleh karena itu mereka akan membaca variabel global setelah diubah. Mari kita sebut ini "tatanan alami", tidak didefinisikan oleh standar apa pun tetapi oleh logika murni.

Namun dalam proses optimasi, kompiler akan mencoba menghilangkan langkah menengah menyimpan sementara nilai di suatu tempat dan mencoba untuk menulis hasil fungsi secara langsung ke tujuan akhir dan dalam kasus itu, mereka sering harus membaca indeks terlebih dahulu , misalnya ke register, untuk dapat langsung memindahkan hasil fungsi ke array. Ini dapat menyebabkan variabel global dibaca sebelum diubah.

Jadi pada dasarnya ini adalah perilaku yang tidak terdefinisi dengan properti yang sangat buruk sehingga kemungkinan hasilnya akan berbeda, tergantung pada apakah optimasi dilakukan dan seberapa agresif optimasi ini. Adalah tugas Anda sebagai pengembang untuk menyelesaikan masalah tersebut dengan mengode:

int idx = global_var;
arr[idx] = update_three(2);

atau coding:

int temp = update_three(2);
arr[global_var] = temp;

Sebagai aturan praktis yang baik: Kecuali jika variabel global const(atau tidak tetapi Anda tahu bahwa tidak ada kode yang akan mengubahnya sebagai efek samping), Anda tidak boleh menggunakannya secara langsung dalam kode, seperti di lingkungan multi-threaded, bahkan ini dapat didefinisikan:

int result = global_var + (2 * global_var);
// Is not guaranteed to be equal to `3 * global_var`!

Karena kompiler dapat membacanya dua kali dan utas lainnya dapat mengubah nilai di antara keduanya. Namun, sekali lagi, pengoptimalan pasti akan menyebabkan kode hanya membacanya sekali, jadi Anda mungkin lagi memiliki hasil berbeda yang sekarang juga tergantung pada waktu utas lainnya. Dengan demikian Anda akan memiliki sakit kepala yang jauh lebih sedikit jika Anda menyimpan variabel global ke variabel stack sementara sebelum digunakan. Perlu diingat jika kompiler berpikir ini aman, kemungkinan besar akan mengoptimalkan bahkan itu jauh dan alih-alih menggunakan variabel global secara langsung, jadi pada akhirnya, itu mungkin tidak membuat perbedaan dalam kinerja atau penggunaan memori.

(Untuk berjaga-jaga jika seseorang bertanya mengapa ada orang yang melakukan x + 2 * xalih - alih 3 * x- pada beberapa penambahan CPU sangat cepat dan begitu juga perkalian dengan kekuatan dua karena kompiler akan mengubahnya menjadi bit shift ( 2 * x == x << 1), namun perkalian dengan angka sewenang-wenang bisa sangat lambat , jadi alih-alih mengalikannya dengan 3, Anda mendapatkan kode yang jauh lebih cepat dengan menggeser bit x dengan 1 dan menambahkan x ke hasilnya - dan bahkan trik itu dilakukan oleh kompiler modern jika Anda mengalikan 3 dan mengaktifkan optimasi agresif kecuali target modern CPU di mana multiplikasi sama cepatnya dengan penambahan sejak saat itu triknya akan memperlambat perhitungan.)


2
Ini bukan perilaku yang tidak terdefinisi - standar mencantumkan kemungkinan dan salah satu dari mereka dipilih kapan saja
Antti Haapala

Kompiler tidak akan berubah 3 * xmenjadi dua bacaan x. Mungkin membaca x sekali dan kemudian melakukan metode x + 2 * x pada register itu dibaca x ke
MM

6
@Mecki "Jika Anda tidak bisa mengatakan apa hasilnya dengan hanya melihat kode, hasilnya tidak terdefinisi" - perilaku tidak terdefinisi memiliki arti yang sangat spesifik dalam C / C ++, dan bukan itu. Penjawab lain telah menjelaskan mengapa contoh khusus ini tidak ditentukan , tetapi tidak ditentukan .
marcelm

3
Saya menghargai niat untuk menerangi bagian dalam komputer, bahkan jika itu melampaui ruang lingkup pertanyaan aslinya. Namun, jargon C / C ++ UB sangat tepat dan harus digunakan dengan hati-hati, terutama ketika pertanyaannya adalah tentang teknis bahasa. Anda dapat mempertimbangkan menggunakan istilah "perilaku tidak spesifik" yang tepat, yang akan meningkatkan jawaban secara signifikan.
kuroi neko

2
@Mecki " Undefined memiliki arti yang sangat istimewa dalam bahasa Inggris " ... tetapi dalam sebuah pertanyaan berlabel language-lawyer, di mana bahasa tersebut memiliki "makna yang sangat istimewa" untuk undefined , Anda hanya akan menimbulkan kebingungan dengan tidak menggunakan definisi bahasa.
TripeHound

-1

Sunting global: maaf kawan, saya semua bersemangat dan menulis banyak omong kosong. Hanya kakek tua mengoceh.

Saya ingin percaya bahwa C telah dihindarkan, tetapi sayangnya sejak C11 telah disejajarkan dengan C ++. Rupanya, mengetahui apa yang akan dilakukan oleh kompiler dengan efek samping dalam ekspresi sekarang harus menyelesaikan sedikit teka-teki matematika yang melibatkan urutan sebagian urutan kode berdasarkan "terletak sebelum titik sinkronisasi".

Saya kebetulan telah merancang dan menerapkan beberapa sistem tertanam waktu-nyata yang kritis pada masa K&R (termasuk pengontrol mobil listrik yang dapat mengirim orang menabrak tembok terdekat jika mesin tidak disimpan dalam pengawasan, sebuah industri 10 ton robot yang dapat menekan orang ke bubur kertas jika tidak diperintahkan dengan benar, dan lapisan sistem yang, meskipun tidak berbahaya, akan memiliki beberapa lusin prosesor menyedot bus data mereka kering dengan overhead sistem kurang dari 1%).

Saya mungkin terlalu pikun atau bodoh untuk mendapatkan perbedaan antara tidak ditentukan dan tidak ditentukan, tapi saya pikir saya masih memiliki ide yang cukup bagus tentang apa yang dimaksud dengan eksekusi secara bersamaan dan akses data. Menurut pendapat saya, informasi tentang C ++ dan sekarang orang-orang C dengan bahasa peliharaan mereka mengambil alih masalah sinkronisasi adalah mimpi pipa yang mahal. Entah Anda tahu apa eksekusi serentak itu, dan Anda tidak memerlukan alat-alat ini, atau tidak, dan Anda akan melakukan kebaikan secara umum untuk tidak mencoba mengacaukannya.

Semua truk dengan abstraksi penghalang memori yang memikat ini hanya disebabkan oleh keterbatasan sementara dari sistem cache multi-CPU, yang semuanya dapat dengan aman dienkapsulasi dalam objek sinkronisasi OS umum seperti, misalnya, variabel dan variabel kondisi C ++ penawaran.
Biaya enkapsulasi ini hanyalah penurunan satu menit dalam kinerja dibandingkan dengan apa yang dapat dicapai oleh penggunaan instruksi CPU spesifik berbutir halus.
Kata volatilekunci (atau a#pragma dont-mess-with-that-variableuntuk semua saya, sebagai pemrogram sistem, perawatan) sudah cukup untuk memberitahu kompiler untuk berhenti memesan kembali akses memori. Kode optimal dapat dengan mudah diproduksi dengan arahan asm langsung untuk menaburkan driver tingkat rendah dan kode OS dengan instruksi khusus CPU ad hoc. Tanpa pengetahuan mendalam tentang bagaimana perangkat keras yang mendasarinya (sistem cache atau antarmuka bus) bekerja, Anda terikat untuk menulis kode yang tidak berguna, tidak efisien atau salah.

Penyesuaian satu menit volatilekata kunci dan Bob akan menjadi semua orang kecuali paman programmer tingkat rendah yang paling keras. Alih-alih itu, geng matematika C ++ biasa memiliki hari lapangan merancang abstraksi lain yang tidak bisa dipahami, menghasilkan kecenderungan khas mereka untuk merancang solusi mencari masalah yang tidak ada dan salah mengartikan definisi bahasa pemrograman dengan spesifikasi kompiler.

Hanya kali ini perubahan diperlukan untuk merusak aspek fundamental C juga, karena "hambatan" ini harus dihasilkan bahkan dalam kode C tingkat rendah untuk bekerja dengan baik. Itu, antara lain, menimbulkan malapetaka dalam definisi ekspresi, tanpa penjelasan atau justifikasi apa pun.

Sebagai kesimpulan, fakta bahwa kompiler dapat menghasilkan kode mesin yang konsisten dari potongan C yang absurd ini hanyalah konsekuensi yang jauh dari cara orang-orang C ++ mengatasi potensi ketidakkonsistenan sistem cache di akhir tahun 2000-an.
Itu membuat kekacauan yang mengerikan dari satu aspek mendasar dari C (definisi ekspresi), sehingga sebagian besar programmer C - yang tidak peduli tentang sistem cache, dan memang begitu - sekarang dipaksa mengandalkan guru untuk menjelaskan perbedaan antara a = b() + c()dan a = b + c.

Mencoba menebak apa yang akan terjadi pada array yang tidak menguntungkan ini adalah kehilangan waktu dan usaha. Terlepas dari apa yang akan dibuat oleh kompiler, kode ini secara patologis salah. Satu-satunya hal yang bertanggung jawab untuk dilakukan adalah mengirimkannya ke tempat sampah.
Secara konseptual, efek samping selalu dapat dipindahkan dari ekspresi, dengan upaya sepele membiarkan modifikasi terjadi sebelum atau setelah evaluasi, dalam pernyataan terpisah.
Jenis kode menyebalkan ini mungkin dibenarkan pada tahun 80-an, ketika Anda tidak dapat mengharapkan kompiler untuk mengoptimalkan apa pun. Tapi sekarang kompiler telah lama menjadi lebih pintar dari kebanyakan programmer, yang tersisa hanyalah sepotong kode buruk.

Saya juga gagal memahami pentingnya debat yang tidak ditentukan / tidak ditentukan ini. Entah Anda bisa mengandalkan kompiler untuk menghasilkan kode dengan perilaku yang konsisten atau Anda tidak bisa. Apakah Anda menyebutnya tidak terdefinisi atau tidak spesifik, sepertinya ini adalah titik perdebatan.

Menurut pendapat saya yang bisa dibilang informasi, C sudah cukup berbahaya dalam kondisi K&R-nya. Evolusi yang bermanfaat adalah menambahkan langkah-langkah keamanan akal sehat. Sebagai contoh, dengan menggunakan alat analisis kode canggih ini, spesifikasi memaksa kompiler untuk mengimplementasikan setidaknya menghasilkan peringatan tentang kode gila, alih-alih diam-diam menghasilkan kode yang berpotensi tidak dapat diandalkan hingga ekstrem.
Tetapi sebaliknya orang-orang memutuskan, misalnya, untuk menentukan urutan evaluasi tetap di C ++ 17. Sekarang setiap perangkat lunak imbecile secara aktif dihasut untuk menempatkan efek samping dalam kodenya secara sengaja, berdasarkan kepastian bahwa kompiler baru akan dengan bersemangat menangani kebingungan dengan cara deterministik.

K&R adalah salah satu keajaiban dunia komputasi. Untuk dua puluh dolar Anda mendapatkan spesifikasi bahasa yang komprehensif (saya telah melihat satu orang menulis kompiler lengkap hanya menggunakan buku ini), sebuah manual referensi yang sangat baik (daftar isi biasanya akan mengarahkan Anda dalam beberapa halaman jawaban untuk jawaban Anda). pertanyaan), dan buku teks yang akan mengajarkan Anda untuk menggunakan bahasa dengan cara yang masuk akal. Lengkap dengan alasan, contoh dan kata-kata bijak peringatan tentang berbagai cara Anda dapat menyalahgunakan bahasa untuk melakukan hal-hal yang sangat, sangat bodoh.

Menghancurkan warisan itu hanya karena sedikit sekali keuntungan bagiku bagaikan pemborosan yang kejam bagiku. Tetapi sekali lagi saya mungkin gagal memahami intinya sepenuhnya. Mungkin beberapa jenis jiwa dapat mengarahkan saya ke arah contoh kode C baru yang mengambil keuntungan signifikan dari efek samping ini?


Ini adalah perilaku yang tidak terdefinisi jika ada efek samping pada objek yang sama dalam ekspresi yang sama, C17 6.5 / 2. Ini tidak diikuti sesuai C17 6.5.18 / 3. Tetapi teks dari 6.5 / 2 "Jika efek samping pada objek skalar tidak diikutsertakan relatif terhadap efek samping yang berbeda pada objek skalar yang sama atau perhitungan nilai menggunakan nilai objek skalar yang sama, perilaku tidak terdefinisi." tidak berlaku, karena perhitungan nilai di dalam fungsi diurutkan baik sebelum atau setelah akses indeks array, terlepas dari operator penugasan memiliki operand yang tidak diikuti itu sendiri.
Lundin

Panggilan fungsi bertindak seperti "mutex terhadap akses yang tidak dilakukan", jika Anda mau. Mirip dengan omong kosong seperti operator koma 0,expr,0.
Lundin

Saya pikir Anda percaya para penulis Standar ketika mereka berkata "Perilaku tidak terdefinisi memberikan lisensi implementor untuk tidak menangkap kesalahan program tertentu yang sulit untuk didiagnosis. Ini juga mengidentifikasi area-area yang mungkin memiliki ekstensi bahasa yang sesuai: implementor dapat menambah bahasa dengan menyediakan definisi perilaku yang secara resmi tidak terdefinisi . " dan mengatakan Standar tidak seharusnya merendahkan program yang bermanfaat yang tidak sepenuhnya sesuai. Saya pikir sebagian besar penulis Standar akan menganggapnya jelas bahwa orang yang mencari untuk menulis kompiler berkualitas ...
supercat

... harus berusaha menggunakan UB sebagai peluang untuk membuat kompiler mereka bermanfaat bagi pelanggan mereka. Saya ragu ada yang bisa membayangkan bahwa penulis kompiler akan menggunakannya sebagai alasan untuk menanggapi keluhan "Kompiler Anda memproses kode ini kurang bermanfaat daripada orang lain" dengan "Itu karena Standar tidak mengharuskan kami untuk memprosesnya dengan bermanfaat, dan implementasi yang bermanfaat memproses program yang perilakunya tidak diamanatkan oleh Standar hanya mempromosikan penulisan program yang rusak ".
supercat

Saya gagal memahami maksud dari pernyataan Anda. Mengandalkan perilaku spesifik-kompiler adalah jaminan tidak mudah dibawa. Ini juga membutuhkan kepercayaan besar pada pabrikan pembuat kompiler, yang dapat menghentikan "definisi ekstra" ini kapan saja. Satu-satunya hal yang dapat dilakukan oleh kompiler adalah menghasilkan peringatan, yang dapat diputuskan oleh programmer yang bijaksana dan berpengetahuan luas untuk menangani kesalahan yang serupa. Masalah yang saya lihat dengan monster ISO ini adalah membuat kode mengerikan seperti contoh OP itu sah (untuk alasan yang sangat tidak jelas, dibandingkan dengan definisi ekspresi K&R).
kuroi neko
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.