4 poin oleh GN⁺ 2023-10-10 | 1 komentar | Bagikan ke WhatsApp
  • Memasuki 2023, cara menulis kode C berubah cukup besar, dan gayanya ditata ulang dengan berfokus pada nama tipe pendek, menyingkirkan string null-terminated, pengembalian struct, dan kompilasi unit translasi tunggal
  • Alias pendek seperti u8, i32, size, s8 serta penghapusan const dan struct dipilih untuk mengurangi noise visual dan beban kognitif dari deklarasi yang berulang
  • String ditangani bukan dengan cara null-terminated, melainkan sebagai fat pointer s8 yang memiliki data dan len, dan pada lingkungan Win32·UTF-16, c16 dan s16 juga digunakan bersama
  • Dalam desain fungsi, pengembalian struct lebih disukai daripada parameter out, dan digunakan pola nilai kembalian yang diinisialisasi nol lalu ok hanya diset saat berhasil
  • Hingga makro, assert, deklarasi Win32, dan inline assembly, aturan lokal yang mudah dibaca diprioritaskan, tetapi saat berkontribusi ke proyek lain, gaya proyek tersebut tetap diikuti

Menunjukkan niat dengan nama tipe pendek

  • Untuk tipe integer·karakter·ukuran pointer dasar, digunakan alias pendek
    • Contoh: u8, c16, b32, i32, u32, u64, f32, f64, uptr, byte, size, usize
  • Nama-nama ini sering muncul di seluruh program, sehingga keringkasan memberi keuntungan langsung saat membaca dan meninjau kode
  • Sufiks _t tidak lagi digunakan, dan kini terasa sebagai elemen yang mengganggu secara visual
  • Untuk prefiks tipe signed, i lebih disukai daripada s
    • s dibiarkan tersedia untuk nama tipe string
  • Untuk tipe ukuran, digunakan size alih-alih isize
    • Karena signed size dianggap sebagai nilai default dasar yang lebih penting
    • usize terutama dipakai dalam kasus sempit saat berinteraksi dengan antarmuka eksternal
  • b32 menunjukkan niat sebagai “boolean 32-bit”
    • Menggunakan ukuran word yang terasa alami alih-alih _Bool
    • Dalam praktiknya, nilainya sering dianggap berada di register atau masuk ke padding struct
    • Saat memori benar-benar penting, boolean dipadatkan ke dalam variabel flags
  • c16 adalah tipe karakter untuk UTF-16 yang diperlukan di Win32
    • Bila berbasis char16_t, debugger seperti GDB lebih mudah menampilkannya sebagai data karakter
    • Nama tipe resmi Win32 adalah wchar_t, tetapi penulisan yang secara eksplisit menyatakan UTF-16 lebih disukai
  • u8 digunakan untuk octet dan terutama data UTF-8, sedangkan byte dibedakan untuk memori mentah dan aliasing type khusus
  • Kekhawatiran terhadap sistem yang tidak mendukung tipe fixed-width dianggap tidak terlalu praktis
    • Nama tipe panjang seperti int_fast32_t juga dianggap pemborosan yang tidak perlu
  • Saat hanya menampilkan potongan kode secara terpisah, alias ini tidak dipakai sendiri
    • Karena pembaca juga memerlukan typedef untuk memahami konteksnya

Aturan makro dan assert

  • Makro bergaya fungsi menggunakan huruf kecil
    • Contoh: countof(a), lengthof(s), new(a, t, n)
  • Untuk konstanta tetap lebih disukai ALL_CAPS, tetapi makro bergaya fungsi dianggap lebih enak dibaca dalam huruf kecil
  • Makro bergaya fungsi juga memiliki masalah namespace yang lebih kecil daripada makro biasa
    • Makro new() dan variabel·field new bisa ada bersamaan
    • Karena hanya diperluas sebagai makro bila berbentuk pemanggilan fungsi
  • Makro assert untuk GCC dan Clang menggunakan bentuk while (!(c)) __builtin_unreachable()
  • Cara assert ini tidak memerlukan pemisahan konfigurasi build
    • Tidak perlu definisi terpisah untuk build debug dan rilis
    • Apakah ia aktif atau tidak dikendalikan oleh keberadaan Undefined Behavior Sanitizer, yaitu UBSan
    • libubsan menyediakan keluaran diagnostik yang mencakup nama file dan nomor baris
    • Dalam build rilis, ini menjadi petunjuk optimisasi yang praktis
  • Untuk mengaktifkan assertion di build rilis, gunakan mode trap UBSan dengan -fsanitize-trap dan aktifkan setidaknya -fsanitize=unreachable
  • Secara teori -funreachable-traps juga bisa, tetapi pada saat penulisan, beberapa rilis GCC terbaru sedang rusak

Hal-hal yang dikurangi dalam deklarasi

  • const tidak digunakan pada parameter
    • Dianggap tidak punya peran praktis dalam optimisasi
    • Disebut sulit mengingat kasus nyata ketika itu menangkap kesalahan atau akan menangkap kesalahan
    • Nama parameter yang baik dianggap sudah cukup sebagai dokumentasi prototipe
  • Penghapusan const adalah perubahan yang meningkatkan produktivitas dengan mengurangi beban kognitif dan noise visual
  • Sebagai pengecualian kecil, const tetap disukai sebagai petunjuk untuk menempatkan tabel statis ke memori read-only dekat kode
    • Jika perlu, const dihilangkan lewat cast
  • Untuk null pointer, digunakan literal 0
    • Ini adalah gaya yang sudah dipakai sekitar 7 tahun
    • Meski ada kemungkinan cacat teoretis, tidak ada kejadian nyata yang terlihat dalam ratusan ribu baris kode
  • restrict hanya dipakai saat diperlukan
    • Kode disusun misalnya dengan tidak memakai parameter out di dalam loop, atau dengan menghindari parameter out sejak awal
  • inline tidak digunakan
    • Karena semuanya dikompilasi sebagai satu unit translasi
  • Semua struct di-typedef
    • Menghapus keyword struct membuat kode lebih mudah dibaca
    • Untuk struct rekursif, forward declaration diletakkan tepat di atasnya, dan field menggunakan nama pendek
  • Semua fungsi selain entry point dideklarasikan sebagai static
    • Karena diasumsikan kompilasi unit translasi tunggal
  • Berkat nama tipe pendek, penghapusan const, dan penghapusan struct, tipe kembalian fungsi dan nama fungsi bisa diletakkan nyaman pada baris yang sama
  • Pernah ada masa nama tipe ditulis dengan huruf besar, tetapi akhirnya ditinggalkan

String memakai s8 alih-alih null-terminated

  • Salah satu perubahan paling produktif adalah menolak sepenuhnya string null-terminated dan menggunakan tipe string s8 yang memiliki data dan len
  • Struktur s8:
    • u8 *data
    • size len
  • Makro s8(s) membungkus literal string C menjadi string s8
  • s8 diteruskan dan dikembalikan sebagai nilai seperti fat pointer
  • s8 juga cocok dipakai sebagai prefiks fungsi
    • Karena nama keluarga str sudah direservasi
    • Contoh: s8span, s8equals, s8compare, s8hash, s8trim, s8clone
  • Untuk membandingkan literal, digunakan bentuk seperti s8equals(tagname, s8("body"))
  • Pernah dicoba juga cara mengikat ukuran dan array dalam satu alokasi dengan flexible array member, tetapi dianggap kurang fleksibel sehingga kekurangannya lebih besar daripada kelebihannya
  • Ada juga kasus ketika sempat mengira program sederhana tidak memerlukan tipe string, tetapi kebanyakan ternyata penilaian itu keliru
  • s16 juga digunakan sebagai tipe pendukung UTF-16
    • Memiliki c16 *data dan size len
    • Soal cara menambahkan u pada literal di makro masih belum sepenuhnya meyakinkan

Pengembalian struct dan cara inisialisasi

  • Pengembalian struct lebih disukai daripada parameter out
    • Secara praktis ini adalah cara mengembalikan beberapa nilai, walau tanpa destructuring
  • Contoh i32parse(s8) mengembalikan hasil parsing value dan status ok sekaligus
  • Biaya salinan tambahan dianggap bukan masalah besar dalam praktik
    • Karena calling convention dapat mengubahnya menjadi parameter out restrict tersembunyi
    • Atau jika di-inline, overhead nilai kembalian menjadi tidak berarti
  • Pendekatan ini mengurangi godaan untuk menandai error dengan sinyal in-band seperti nilai null khusus
  • Disukai pola membuat nilai kembalian yang diinisialisasi nol di awal fungsi lalu memakainya untuk semua return
    • Saat error, fungsi langsung mengembalikan keadaan nol tersebut
    • Pada jalur sukses, ok diset ke true tepat sebelum return
  • Selain data statis dan makro s8·s16, penggunaan initializer juga dikurangi
    • Designated initializer pun dihindari, dan inisialisasi dilakukan lewat assignment
  • Inisialisasi dengan assignment dianggap mudah dibaca, dan di antara tiap assignment ada sequence point yang memberi urutan eksplisit
  • Pada inisialisasi yang hasilnya bisa dipengaruhi urutan pemanggilan, seperti fungsi pembangkit bilangan acak, jadi tidak perlu memikirkan kemungkinan nilai yang mungkin muncul

Deklarasi Win32 dan inline assembly

  • __attribute__ kurang disukai dibanding __attribute
    • Sufiks __ di belakang dianggap berlebihan dan tidak perlu
  • Dalam pemrograman sistem Win32, windows.h tidak di-include; prototipe yang dibutuhkan ditulis langsung
    • Karena biasanya jumlah deklarasi dan definisi yang diperlukan tidak banyak
    • Ini mengurangi waktu build dan membuat namespace tidak terlalu berantakan
    • Juga lebih cocok dipadukan dengan tipe kustom yang lebih rapi seperti u32, b32, uptr daripada DWORD, BOOL, ULONG_PTR
  • Contoh deklarasi Win32 memakai makro W32(r) __declspec(dllimport) r __stdcall
    • Fungsi seperti ExitProcess, GetStdHandle, VirtualAlloc, WriteConsoleA, WriteConsoleW dideklarasikan langsung
  • Dalam inline assembly, tanda kurung luar diperlakukan seperti kurung kurawal
    • Seperti pada if, diberi spasi sebelum tanda kurung buka
    • Tiap baris constraint dimulai dengan titik dua
  • Contoh kecil untuk melihat gaya yang disebutkan tersedia di wordhist.c
  • Contoh yang sedikit lebih besar adalah implementasi bahasa pemrograman mini asmint.c

1 komentar

 
GN⁺ 2023-10-10
Pendapat Hacker News
  • Tampaknya mereka menganggap #define sizeof(x) (size)sizeof(x) tidak perlu tanda kurung luar, tetapi ada satu pengecualian yang sangat kecil
    Casting memiliki prioritas lebih tinggi daripada perkalian, jadi sizeof(x) * 3 berjalan aman sebagai (size)sizeof(x) * 3
    Namun pada (size)sizeof(x)[y], array indexing diterapkan lebih dulu daripada casting, sehingga menjadi (size)(sizeof(x)[y]), bukan ((size)sizeof(x))[y]
    Dalam kode nyata, hampir tidak ada alasan untuk melakukan indexing pada sizeof(x), tetapi karena C mengizinkan integer[pointer] bermakna sama dengan pointer[integer], makro ini bisa tetap terkompilasi namun bekerja keliru karena kurang tanda kurung
    Yang lebih mendasar, saya juga sulit setuju dengan klaim bahwa signed size lebih baik. Penulis mengatakan unsigned size adalah sumber cacat, tetapi kode yang diberikan juga punya bug yang merusak memori jika count bernilai negatif
    Pada integer tanpa tanda, jumlah negatif tidak bisa direpresentasikan, dan jika terjadi overflow nilainya menjadi bilangan positif yang sangat besar sehingga tertangkap oleh pemeriksaan yang ada. Secara pribadi, saya lebih suka memakai integer tanpa tanda, tetapi sebisa mungkin menggunakan wrapper pemeriksa rentang yang menghentikan program saat overflow

    • Saya justru menyukai semantik _Bool
      Karena ekspresi yang bekerja baik di if (flags & FLAG_ALLOCATED) bisa diekstrak menjadi variabel boolean seperti _Bool need_free = flags & FLAG_ALLOCATED;
      flags & FLAG_ALLOCATED saat disetel bisa bernilai sembarang nilai non-nol, bukan 1, dan _Bool menormalkannya menjadi 1. Jika ditampung sebagai int, if (need_free) akan lolos, tetapi if (need_free == true) bisa gagal
      Ada juga kekurangannya. Saat refactoring, jika melewatkan fakta bahwa konversi implisit ke _Bool sebelumnya melakukan sesuatu yang berguna, kode bisa berubah menjadi kode keliru seperti if ((flags & FLAG_ALLOCATED) == true)
      Selain itu, saat membaca struct dari disk atau mengisi byte sembarang, jika field _Bool bukan 0 atau 1, ada risiko undefined behavior
    • Sebenarnya (size)(sizeof(x)[y]) pun mungkin mengejutkan banyak orang, tetapi itu sama dengan (size)(sizeof ((x)[y]))
      sizeof bukan fungsi, melainkan operator unary, dan indexing serta pemanggilan fungsi punya prioritas lebih tinggi daripada sizeof. Karena itu saya lebih suka memberi spasi setelah sizeof, dan memakai tanda kurung pada operand hanya saat diperlukan
      https://en.cppreference.com/w/c/language/operator_precedence
      Jika ingin menulis makro dengan benar, bentuknya menjadi #define sizeof(x) ((size)(sizeof (x)))
    • Tangkapan yang bagus. Pelajarannya: jika definisi makro tidak diperluas hanya menjadi satu token, selalu bungkus dengan tanda kurung. Aturan prioritas C benar-benar rumit
  • Mendefinisikan tipe sendiri rasanya sudah satu langkah terlalu jauh
    Bahkan orang yang sudah terbiasa dengan tipe C tetap harus mempelajari sistem aneh tersendiri untuk memahami satu program. Menentukan ukuran memang masuk akal, jadi penggunaan uint32_t alih-alih uint bisa dimengerti
    Tipe semacam ini seharusnya didefinisikan di header yang tepat, dan mungkin saya salah karena sudah lama tidak memakai C

    • Secara praktis, int di C adalah 32-bit
      Memang tidak pada target 16-bit, tetapi apakah Anda benar-benar akan mem-porting program 5MB ke 16-bit? Kekhawatiran seperti itu biasanya tidak bernilai
      Masalahnya adalah long. Di sebagian mesin 32-bit, di sebagian lain 64-bit, jadi membingungkan. Untungnya long long selalu 64-bit, jadi cukup tinggalkan long
      char 8-bit, short 16-bit, int 32-bit, long long 64-bit, selesai. Di C, kita sudah membuang waktu tanpa henti untuk ukuran int
    • Penulis memang membatasinya sebagai gaya coding pribadi. Sejujurnya tipe standar terlalu bertele-tele, dan saya sempat berpikir akan menyenangkan kalau daftar ringkas yang dia ajukan ini diadopsi sejak dulu
    • Dengan sedikit bercanda, sebagian besar pemrograman adalah berurusan dengan sistem tipe milik orang lain
      Bagi orang yang sering memakai C, singkatan-singkatan di sini akrab, dan untuk sebuah sistem tipe kustom, ini cukup elegan. Mengingatkan pada Rust
    • Tipe seperti itu ada di stdint.h
      Saya selalu heran melihat banyak proyek bersusah payah membuat ulang file ini
      Menerjemahkan ulang tipe standar dengan nama sendiri merepotkan pembaca. Dulu di sebuah proyek C++, saya pernah bertanya mengapa ada begitu banyak typedef untuk koleksi, referensi, dan objek majemuk, lalu dijawab bahwa itu membuatnya lebih mudah dipahami
      Belakangan saya melihat ada cheatsheet typedef tertempel di samping monitor orang itu
    • Definisi tipe integer sendiri memang masuk akal di platform dengan keterbatasan sumber daya
      Sering terlihat tipe seperti dim_t, yang menjadi 32-bit atau 64-bit tergantung penggunaan. Bahkan di platform 64-bit, struktur dengan pointer compression sering memakai integer 32-bit
      Misalnya, jika mengalokasikan heap sendiri dan hanya menyimpan offset 32-bit, untuk beban kerja di bawah 4GB penggunaan memori bisa berkurang setengah, dan cache locality juga membaik sehingga performa meningkat
  • Meninggalkan konvensi C yang sudah mapan hanya karena preferensi pribadi rasanya agak berlebihan
    Memakai u8, i32 alih-alih uint8_t atau int32_t memang mengurangi beberapa karakter, tetapi bisa membingungkan orang lain saat membaca kode
    Memakai tipe string kustom alih-alih string berakhiran null juga terasa meningkatkan kesulitan kolaborasi, mengingat C dibangun dengan berpusat pada string semacam itu
    Menulis sendiri prototipe Win32 API tanpa menyertakan windows.h mungkin bisa mengurangi waktu kompilasi, tetapi rasanya seperti memilih jalan setapak di hutan padahal ada jalan raya yang terawat baik. Banyak di antaranya tampak lebih dekat ke preferensi pribadi daripada kode C yang mudah ditangani semua orang

    • u8 atau i32 bukan soal menghemat jumlah ketikan, melainkan mengurangi beban perseptual saat membaca
      Argumen “jumlah penekanan tombol” yang selalu muncul dalam debat verbositas versus keringkasan itu sangat cacat. Keyakinan bahwa keringkasan hanya baik untuk mengetik cepat, sedangkan verbositas selalu lebih baik untuk dibaca, itu keliru
      Verbositas juga punya kelebihan untuk pemahaman bacaan, tetapi keringkasan juga punya kelebihan, dan tidak ada pihak yang jelas-jelas menang. Keduanya hanyalah kompromi yang berbeda
    • Nama seperti u16 banyak dipakai, dan kecil kemungkinannya membingungkan programmer
      Titik yang benar-benar runtuh adalah ketika dua program berbeda masing-masing mendefinisikan u16 dan mengeksposnya di file header, lalu program ketiga menyertakan kedua header itu sekaligus
      Tipe library yang memakai namespace akan berbentuk seperti libname_u32, dan kalau sudah begitu rasanya lebih ingin memakai uint32_t saja daripada prefiks libname_
    • Kemungkinan programmer C yang kompeten menjadi bingung saat melihat u8 atau i32 paling-paling hanya teoretis, dan tampak agak seperti strawman
      Mereka mungkin kesal, tetapi bukan bingung. Seperti kata Rich Hickey, sebelum belajar cara membacanya, semua hal sulit dibaca
  • Katanya memakai boolean 32-bit mungkin terlihat seperti pemborosan memori bagi pemula; kalau begitu sepertinya saya juga pemula
    Saya pernah mendengar beberapa kasus ketika itu tidak lebih buruk daripada bool 8-bit, tetapi tidak melihat kasus ketika itu benar-benar lebih baik. Jika ada boolean yang berdekatan di struct, atau variabel boolean sebuah fungsi terdorong dari register ke stack, tetap saja memori terbuang
    Meski hanya beberapa byte, saya tidak mengerti mengapa sengaja melakukan pesimisasi. Apa yang didapat dari memakai ukuran yang lebih besar?

    • Sepenuhnya bergantung pada arsitektur dan CPU, tetapi dari pengalaman masa lalu, contoh yang jelas adalah pekerjaan pemrosesan numerik
      Bentuknya ada nilai kondisi di depan struct per siklus, lalu di belakangnya diikuti 512, 1024, atau 2048 nilai sampel. Seorang junior ingin menghemat ruang, jadi ia mem-packing struct dan membuat nilai kondisi menjadi 8-bit 1 byte
      Kode yang “ditingkatkan” itu menurunkan throughput sekitar 10 kali di chip Intel, dan memicu BUS ERROR pada arsitektur SPARC RISC
      Karena header struct di-packing, array data menjadi tidak selaras; Intel diam-diam harus mengambil dua word 32-bit dan menggabungkannya, sedangkan SPARC marah secara wajar terhadap data yang tidak selaras
      Jika itu komputasi pipeline yang mementingkan throughput, bukan penyimpanan file jangka panjang, terkadang lebih baik menyesuaikan data dengan alignment arsitektur daripada mem-packing-nya demi “menghemat ruang”
    • Biasanya optimasi yang mudah adalah memberi padding pada field struct agar sesuai dengan batas 32-bit
      Hampir semua compiler melakukannya, jadi cukup cari “structure alignment/padding”. Jika compiler toh akan menyisakan ruang kosong, lebih baik memakai memori itu sendiri; jika tidak, Anda bisa kehilangan performa
      Lebih tepatnya, setiap field harus berada pada alamat yang habis dibagi ukuran dirinya atau ukuran wordline, dan keseluruhan struct juga harus di-padding menjadi kelipatan ukuran field terbesar. Dalam praktiknya, biasanya ini berarti alignment 32-bit
      Referensi: http://www.catb.org/esr/structure-packing/
    • Jika memakai tipe bool yang sebenarnya, sanitizer akan memberi peringatan ketika nilainya bukan 0 untuk false atau 1 untuk true
    • Arsitektur komputer umumnya dioptimalkan untuk akses selaras 32-bit atau lebih. Yang didapat biasanya, meski tidak selalu, adalah performa
    • Saya penasaran seperti apa contoh fungsi ketika variabel boolean terdorong ke stack dan 3 byte itu menjadi penting
  • Saya tidak setuju dengan klaim soal pengembalian struct dan parameter output
    Itu membuat fungsi yang bisa mengembalikan error jauh lebih sulit dikomposisikan, dan tipe bertambah di mana-mana. Dalam praktiknya hampir semua fungsi bisa gagal, jadi gaya pengembalian error yang dapat diprediksi lebih penting, terutama jika sampai menangani kehabisan memori

    • Hampir tidak ada yang menangani kehabisan memori
      Itu sangat sulit dan manfaatnya juga hampir tidak ada. Pada titik itu, masalah yang muncul jauh berbeda daripada pilihan gaya pemrograman
    • Jika unpacking struct yang bermakna dimungkinkan, kita mungkin bisa mendapatkan semantik errno biasa dan parameter output, tetapi C tidak memilikinya
      Meski begitu, mengomposisikan nilai opsional di C memang selalu agak menyakitkan. Jika tidak memakai exception, di banyak bahasa exception dan monad tampak seperti dua pilihan utama, tetapi keduanya tidak cocok di C dan juga tidak cocok dengan filosofi sebagian besar programmer C
      Untuk pemanggilan satu-ke-satu yang sederhana, makro bisa dicoba, tetapi ada batasnya. Meski C++ mengerikan, C++ optional lebih menyenangkan dipakai daripada if(foo(x,y, out1, out2) != WHATEVER_LIBRARY_OK) { ... }
    • Mengembalikan option atau sum type memang tepat, tetapi di C itu sangat merepotkan untuk ditulis
      Pola menambahkan if (thing(...)) goto fail pada setiap pemanggilan fungsi juga tidak tampak terlalu bagus, meski pihak Go tampaknya menyukainya
      Atau ada thread_local mylibrary_errno, yang di dalam library mungkin memang cara yang tepat, lalu di batasnya bisa dikonversi menjadi nilai balik enum
  • Saat membaca sampai bagian “signed sizes are the way”, rasanya cukup berhenti di situ saja
    signed size adalah kebocoran abstraksi yang sangat mengejutkan dan cara yang mengundang bencana
    Pernyataan bahwa const tidak punya peran praktis dan tidak pernah membantu menangkap kesalahan juga sulit diterima. Orang sering tertukar antara buffer input dan buffer output, dan const langsung menampakkannya
    Anjuran agar semua fungsi selain entry point dibuat static juga bisa membuat kita mengutuk penulisnya saat debugging karena tidak bisa menemukan variabel atau fungsi
    Preferensi mengembalikan struct rentan membuat orang tanpa sengaja mengembalikan pointer stack dan membuka celah keamanan besar. Jika meneruskan buffer output, semantik kepemilikan jadi jelas
    Saran ini mungkin cukup cocok bagi orang yang terutama menulis kode sistem 64-bit, tetapi di ranah embedded 32-bit bisa cepat menjadi masalah

    • Bjarne Stroustrup menulis memo terperinci yang membela signed size
      https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p14...
    • Orang yang membenci const tidak akan pernah puas, jadi pakai saja const di tempat yang perlu, propagasikan seperlunya, lalu abaikan keluhannya
      Kalau mereka menghapusnya, masukkan lagi. Selalu pihak mereka yang lebih dulu lelah. Sudah 25 tahun saya melakukan ini dan masih bertahan
      static bisa bergantung pada tool. Sekitar 15 tahun lalu saya beralih ke default static dan penggunaan size_t secara menyeluruh, dan sejauh ini belum mengalami masalah
  • Menarik bahwa pengalaman saya justru mengarah ke arah lain
    https://dlang.org/blog/2023/10/02/crafting-self-evident-code...
    Artikelnya berpusat pada D, tetapi prinsipnya juga berlaku untuk C

    • Bacaan yang menarik
      Bagian tentang memindahkan ekspresi kondisi ke dalam doX() dan doZ() menarik. Saya tidak tahu apakah itu selalu benar; tergantung di mana abstraksi diletakkan dan model mental terhadap kodenya
      Misalnya deleteRecords(); tidak lebih baik daripada if let x = deadRecords() deleteRecords(x);. Yang terakhir terlihat lebih berantakan, tetapi ada nilainya karena di bagian depan menunjukkan bahwa ini bukan penghapusan, melainkan pemangkasan
      Kalau fungsi diganti dengan cerdas menjadi seperti pruneDeadProjects(), itu baik-baik saja, tetapi sekadar memindahkan kondisi ke dalam fungsi bisa membuat konteks menjadi berbahaya dan menjadi abstraksi yang bocor
  • Saya setuju memakai typedef untuk semua struct karena membantu keringkasan
    Menurut saya typedef boleh digunakan cukup banyak. Namun sebaiknya hanya typedef objeknya sendiri, bukan pointernya. Jika butuh pointer, kapan pun bisa menulis (type *)
    Khususnya untuk function pointer, yang harus di-typedef bukan function pointer, melainkan fungsi itu sendiri. Dengan begitu typedef tersebut juga bisa dipakai pada deklarasi fungsi untuk mendapatkan pemeriksaan tipe parameter, dan saat signature fungsi berubah tidak perlu memperbaiki semua deklarasi
    Kebanyakan codebase C salah melakukan ini dengan men-typedef function pointer, lalu tetap harus menulis deklarasi fungsi yang disesuaikan dengan definisi pointer itu secara manual
    Saya masih belum yakin dengan cara memakai struct sebagai tipe kembalian. Saya lebih suka memakai kode error numerik sebagai nilai kembalian, dan menerima nilai kembalian lainnya melalui parameter output

    • Untuk struct opaque, saya lebih suka memakai typedef untuk meniru kelas yang semua field-nya private, dan memakai struct untuk struktur data sederhana
      Kelas seharusnya hanya diakses lewat fungsi, sedangkan struct seharusnya bisa diakses langsung
      Ini kurang lebih dekat dengan konvensi standar C/POSIX. Misalnya perbedaan antara pthread_t dan struct stat
    • Saya setuju jangan men-typedef pointer itu sendiri
      Hal seperti SDL_net melakukan persis itu, dan saya tidak menyukainya. Sebenarnya pointer, tetapi di-typedef seolah-olah value type
      Saya paham maksudnya, tetapi caranya cukup membuat tidak nyaman
  • Banyak bagian dari artikel ini masuk akal
    Saya mulai menulis bare-metal OS untuk Arm64, dan meski masih tahap awal, saya melakukan hal serupa. Saya memakai string Pascal dan juga mengganti nama tipe. Namun gayanya int8, bukan i8
    Karena saya cepat memutuskan tidak berniat mem-port software nyata, saya tidak perlu mengikuti fungsi atau konvensi library C standar. Berkat itu saya bisa bereksperimen lebih bebas
    C adalah bahasa yang begitu tua sampai beban dari masa ketika setiap byte berharga masih tersisa hingga ke nama fungsi. Senang rasanya bisa lepas dari itu, dan isi artikel ini beserta berbagai perubahan nama kecil terasa seperti perapian yang cukup rapi

    • Kalau tidak perlu mengikuti fungsi atau konvensi library C standar, apakah itu berarti ini sekadar proyek hobi dan tidak akan menjadi sesuatu yang besar dan profesional seperti gnu?
    • Saya belum pernah terpikir bahwa byte juga dihemat pada simbol
  • typedef float f32;, typedef double f64; tampak seperti pijakan yang berbahaya karena mengasumsikan float adalah 32-bit dan double adalah 64-bit
    OpenCV mendefinisikan float16_t, CUDA mengimplementasikan bilangan floating-point presisi setengah, dan mikrokontroler bisa memiliki implementasinya masing-masing
    C++23 memperkenalkan tipe floating-point berlebar tetap, tetapi saya tidak tahu cara memaksakannya di C. Tampaknya lebih baik menyediakan makro untuk memeriksa pada waktu kompilasi bahwa tidak ada kehilangan data
    Secara keseluruhan, seperti yang dikatakan orang lain, demi keterbacaan mungkin lebih baik membiarkan sebagian hal tetap pada default, meskipun tidak seringkas itu
    [0] https://docs.opencv.org/4.x/df/dc9/classcv_1_1float16__t.htm...
    [1] https://docs.nvidia.com/cuda/cuda-math-api/group__CUDA__MATH...
    [2] https://en.cppreference.com/w/cpp/types/floating-point