4 पॉइंट द्वारा GN⁺ 2023-10-10 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • 2023 में C code लिखने का मेरा तरीका काफ़ी बदल गया, और अब मेरी style मुख्यतः छोटे type names, null-terminated strings से परहेज़, struct return, और single translation unit compilation के इर्द-गिर्द दोबारा व्यवस्थित हुई है
  • u8, i32, size, s8 जैसे छोटे alias, और conststruct को हटाना, बार-बार आने वाली declarations के visual noise और cognitive load को कम करने का चुनाव है
  • strings को null termination की जगह data और len वाले s8 fat pointer के रूप में संभाला जाता है, और Win32·UTF-16 environment में c16 और s16 भी साथ इस्तेमाल होते हैं
  • function design में out parameters की बजाय struct return को प्राथमिकता दी जाती है, और zero-initialized return value में success के समय ही ok सेट करने वाला pattern इस्तेमाल होता है
  • macros, assert, Win32 declarations, और inline assembly तक, पढ़ने में आसान local rules को महत्व दिया जाता है, लेकिन दूसरे project में योगदान करते समय उसी project की style अपनाई जाती है

छोटे type names से intent स्पष्ट करना

  • basic integer, character, और pointer-size types के लिए छोटे alias इस्तेमाल किए जाते हैं
    • उदाहरण: u8, c16, b32, i32, u32, u64, f32, f64, uptr, byte, size, usize
  • ये नाम पूरे program में बहुत बार आते हैं, इसलिए संक्षिप्तता का पढ़ने और review करने में सीधा लाभ है
  • _t suffix अब इस्तेमाल नहीं किया जाता, क्योंकि अब यह दृश्य रूप से बिखराव पैदा करने वाला तत्व लगता है
  • signed type prefix के रूप में s की बजाय i को पसंद किया जाता है
    • s को string type names के लिए खाली रखा जाता है
  • size type के लिए isize की बजाय size इस्तेमाल होता है
    • क्योंकि signed size को अधिक महत्वपूर्ण default माना जाता है
    • usize का उपयोग मुख्यतः external interface के साथ interaction जैसे सीमित मामलों में होता है
  • b32 नाम “32-bit boolean” वाली intent को स्पष्ट करता है
    • _Bool की बजाय natural word size का उपयोग होता है
    • व्यवहार में यह अक्सर register में होता है या struct padding में चला जाता है
    • जब memory सचमुच महत्वपूर्ण हो, तब boolean को flags variable में compress किया जाता है
  • c16 Win32 में आवश्यक UTF-16 characters के लिए type है
    • अगर यह char16_t पर आधारित हो, तो GDB जैसे debugger उसे character data के रूप में दिखाने में मदद करते हैं
    • Win32 का official type name wchar_t है, लेकिन UTF-16 को स्पष्ट करना अधिक पसंद है
  • u8 का उपयोग octet और मुख्यतः UTF-8 data के लिए होता है, जबकि byte को raw memory और विशेष aliasing type के रूप में अलग रखा जाता है
  • fixed-width types को support न करने वाले system की चिंता को व्यावहारिक रूप से कम महत्व दिया जाता है
    • int_fast32_t जैसे लंबे type names भी अनावश्यक बर्बादी लगते हैं
  • जब केवल code snippet अलग से दिखाया जाता है, तब ये alias अकेले नहीं लिखे जाते
    • क्योंकि reader को context समझने के लिए typedef भी साथ चाहिए होता है

macro और assert नियम

  • function-like macros को lowercase में लिखा जाता है
    • उदाहरण: countof(a), lengthof(s), new(a, t, n)
  • constants के लिए अब भी ALL_CAPS पसंद है, लेकिन function-like macros के लिए lowercase ज़्यादा readable लगता है
  • function-like macros में सामान्य macros की तुलना में namespace समस्या कम होती है
    • new() macro और new variable/field एक साथ रखे जा सकते हैं
    • क्योंकि function-call form न होने पर macro expand नहीं होता
  • GCC और Clang के लिए assert macro, while (!(c)) __builtin_unreachable() रूप का उपयोग करता है
  • इस assert तरीके में अलग build configuration बाँटने की ज़रूरत नहीं पड़ती
    • debug और release build के लिए अलग definitions रखने की ज़रूरत नहीं होती
    • व्यवहार Undefined Behavior Sanitizer यानी UBSan की मौजूदगी से नियंत्रित होता है
    • libubsan file name और line number सहित diagnostic output देता है
    • release build में यह व्यावहारिक optimization hint बन जाता है
  • release build में assertion चालू करने के लिए UBSan को -fsanitize-trap के साथ trap mode में रखें और कम से कम -fsanitize=unreachable चालू करें
  • सैद्धांतिक रूप से -funreachable-traps से भी यह संभव है, लेकिन लिखने के समय तक की कुछ हाल की GCC releases में यह टूटा हुआ है

declarations में क्या कम किया जाता है

  • parameters पर const नहीं लगाया जाता
    • इसे optimization में व्यावहारिक भूमिका वाला नहीं माना जाता
    • ऐसा कोई उदाहरण याद नहीं आया जहाँ इससे गलती पकड़ी गई हो, या पकड़ी जा सकती थी
    • अच्छे parameter names को prototype documentation के लिए पर्याप्त माना जाता है
  • const हटाना cognitive load और visual noise घटाकर productivity बढ़ाने वाला बदलाव रहा है
  • एक छोटा अपवाद यह है कि code के पास static tables को read-only memory में रखने के संकेत के रूप में const अब भी पसंद है
    • ज़रूरत हो तो cast के जरिए const हटाया जाता है
  • null pointer के लिए literal 0 इस्तेमाल होता है
    • यह लगभग 7 साल से इस्तेमाल की जा रही style है
    • सैद्धांतिक खामी की संभावना मानी जाती है, लेकिन लाखों lines के code में इसका वास्तविक उदाहरण नहीं देखा गया
  • restrict केवल ज़रूरत पड़ने पर इस्तेमाल होता है
    • code को इस तरह व्यवस्थित किया जाता है कि out parameters loop में न आएँ, या out parameters से ही बचा जाए
  • inline का उपयोग नहीं किया जाता
    • क्योंकि सब कुछ एक ही translation unit में compile किया जाता है
  • हर struct को typedef किया जाता है
    • struct keyword हटाने से code अधिक readable होता है
    • recursive struct के लिए उसके ठीक ऊपर forward declaration दी जाती है, और fields में छोटे names उपयोग होते हैं
  • entry point को छोड़कर हर function को static घोषित किया जाता है
    • क्योंकि single translation unit compilation को आधार माना गया है
  • छोटे type names, const हटाने, और struct हटाने की वजह से function return type और function name को एक ही line पर आराम से रखा जा सकता है
  • type names को uppercase में लिखने का दौर भी था, लेकिन अंततः उसे छोड़ दिया गया

strings में null termination की जगह s8

  • सबसे productive बदलावों में से एक यह रहा कि null-terminated strings को पूरी तरह छोड़कर data और len वाले s8 string type का उपयोग किया जाने लगा
  • s8 struct:
    • u8 *data
    • size len
  • s8(s) macro, C string literal को s8 string में wrap करता है
  • s8 को fat pointer की तरह value के रूप में pass और return किया जाता है
  • s8 function prefix के रूप में भी अच्छा काम करता है
    • क्योंकि str family के names reserved हैं
    • उदाहरण: s8span, s8equals, s8compare, s8hash, s8trim, s8clone
  • literal comparison के लिए s8equals(tagname, s8("body")) जैसी form उपयोग होती है
  • flexible array member के साथ size और array को एक allocation में बाँधने का प्रयोग भी किया गया, लेकिन इसकी सीमित flexibility को फ़ायदों से बड़ा माना गया
  • कभी-कभी लगा कि छोटे program में string type की ज़रूरत नहीं होगी, लेकिन अधिकतर मामलों में यह आकलन गलत निकला
  • UTF-16 support के लिए s16 भी उपयोग होता है
    • इसमें c16 *data और size len होते हैं
    • macro में literal पर u prefix लगाने के तरीके को लेकर अभी पूरी तरह भरोसा नहीं है

struct return और initialization pattern

  • out parameters की बजाय struct return को पसंद किया जाता है
    • यह व्यावहारिक रूप से multiple values return करने का तरीका है, भले destructuring न हो
  • उदाहरण i32parse(s8) parsing result value और status ok को साथ लौटाता है
  • अतिरिक्त copy cost को व्यवहारिक काम में बड़ा मुद्दा नहीं माना जाता
    • calling convention इसे hidden restrict out parameter में बदल सकती है
    • या inline हो जाने पर return-value overhead का महत्व नहीं रहता
  • यह तरीका error दिखाने के लिए special null return जैसे in-band signal का लालच कम करता है
  • function की शुरुआत में zero-initialized return value बनाकर हर return में उसी का उपयोग करने वाला pattern पसंद किया जाता है
    • error पर तुरंत zero-initialized state में return होता है
    • success path में return से ठीक पहले ok को true किया जाता है
  • static data और s8·s16 macros को छोड़कर initializer का उपयोग भी कम किया जाता है
    • designated initializer से भी बचा जाता है, और assignment statements से initialization किया जाता है
  • assignment-based initialization पढ़ने में आसान है, और हर assignment के बीच sequence point होने से स्पष्ट क्रम मिलता है
  • random number generation जैसे initialization में, जहाँ call order result को प्रभावित कर सकता है, वहाँ संभावित value combinations के बारे में अलग से सोचना नहीं पड़ता

Win32 declarations और inline assembly

  • __attribute__ की बजाय __attribute पसंद किया जाता है
    • आखिर का __ ज़रूरत से ज़्यादा और अनावश्यक लगता है
  • Win32 system programming में windows.h को include नहीं किया जाता, बल्कि ज़रूरी prototypes सीधे लिखे जाते हैं
    • आम तौर पर ज़रूरी declarations और definitions की संख्या बहुत अधिक नहीं होती
    • इससे build time घटता है और namespace भी कम बिखरता है
    • यह DWORD, BOOL, ULONG_PTR की बजाय u32, b32, uptr जैसे custom types के साथ अधिक साफ़ मेल खाता है
  • Win32 declaration उदाहरण में W32(r) __declspec(dllimport) r __stdcall macro का उपयोग होता है
    • ExitProcess, GetStdHandle, VirtualAlloc, WriteConsoleA, WriteConsoleW जैसे functions सीधे declare किए जाते हैं
  • inline assembly में बाहरी parentheses को braces की तरह माना जाता है
    • if की तरह opening parenthesis से पहले space रखा जाता है
    • हर constraint line colon से शुरू होती है
  • इस style को छोटे program में देखने के लिए wordhist.c उदाहरण के रूप में दिया गया है
  • थोड़ा बड़ा उदाहरण mini programming language implementation asmint.c है

1 टिप्पणियां

 
GN⁺ 2023-10-10
Hacker News की रायें
  • ऐसा लगता है कि उन्होंने माना कि #define sizeof(x) (size)sizeof(x) में बाहरी कोष्ठकों की जरूरत नहीं है, लेकिन इसमें एक बेहद छोटा अपवाद है
    casting की precedence multiplication से ज्यादा होती है, इसलिए sizeof(x) * 3, (size)sizeof(x) * 3 के रूप में सुरक्षित तरीके से काम करता है
    लेकिन (size)sizeof(x)[y] में array indexing casting से पहले लागू होती है, इसलिए यह ((size)sizeof(x))[y] नहीं, बल्कि (size)(sizeof(x)[y]) बन जाता है
    असली code में sizeof(x) को index करने की नौबत नहीं आती, लेकिन C में integer[pointer] को pointer[integer] जैसा ही अर्थ रखने की अनुमति है, इसलिए कोष्ठकों की कमी के कारण यह macro compile होकर गलत व्यवहार कर सकता है
    ज्यादा मूल बात यह है कि signed size बेहतर है, इस दावे से भी सहमत होना मुश्किल है। लेखक कहते हैं कि unsigned size खामियों का स्रोत है, लेकिन दिए गए code में भी अगर count negative हो तो memory खराब करने वाला bug है
    unsigned integer में negative count व्यक्त नहीं होता, और overflow होने पर भी वह बहुत बड़ा positive number बन जाता है, जो मौजूदा checks में पकड़ा जाता है। निजी तौर पर मैं unsigned integer इस्तेमाल करने, लेकिन जहां तक संभव हो range-check wrapper से overflow पर रोक देने को तरजीह देता हूं

    • _Bool की semantics मुझे उलटे पसंद है
      क्योंकि if (flags & FLAG_ALLOCATED) में ठीक से काम करने वाली expression को _Bool need_free = flags & FLAG_ALLOCATED; की तरह boolean variable में निकाला जा सकता है
      flags & FLAG_ALLOCATED set होने पर 1 नहीं, बल्कि कोई भी non-zero value हो सकता है, और _Bool इसे 1 में normalize कर देता है। int में लेने पर if (need_free) pass हो जाता है, लेकिन if (need_free == true) fail हो सकता है
      इसकी कमियां भी हैं। refactoring के दौरान अगर आप यह चूक जाएं कि _Bool में implicit conversion कोई उपयोगी काम कर रहा था, तो if ((flags & FLAG_ALLOCATED) == true) जैसा गलत code बन सकता है
      साथ ही disk से struct पढ़ते समय या arbitrary bytes भरते समय अगर _Bool field 0 या 1 नहीं है, तो undefined behavior का जोखिम होता है
    • दरअसल (size)(sizeof(x)[y]) भी कई लोगों को चौंकाएगा, लेकिन यह (size)(sizeof ((x)[y])) के समान है
      sizeof function नहीं, बल्कि unary operator है, और indexing तथा function call की precedence sizeof से ज्यादा होती है। इसलिए मैं sizeof के बाद space रखने और जरूरत पड़ने पर ही operand पर parentheses लगाने का तरीका पसंद करता हूं
      https://en.cppreference.com/w/c/language/operator_precedence
      macro को सही तरीके से लिखना हो तो यह #define sizeof(x) ((size)(sizeof (x))) होगा
    • अच्छी पकड़ है। सबक यह है कि अगर macro definition सिर्फ एक token में expand नहीं होती, तो उसे हमेशा parentheses से घेरना चाहिए। C के precedence rules सचमुच जटिल हैं
  • अपने खुद के types define करना एक कदम ज्यादा आगे जाने जैसा लगता है
    जो लोग पहले से C types से परिचित हैं, उन्हें भी किसी program को समझने के लिए एक अलग, अजीब system सीखना पड़ेगा। size को स्पष्ट करना जायज है, इसलिए uint की बजाय uint32_t इस्तेमाल करने जैसी बात समझ आती है
    ऐसे types किसी उचित header में defined होने चाहिए, और हो सकता है मैं गलत होऊं क्योंकि मैंने लंबे समय से C इस्तेमाल नहीं किया है

    • व्यावहारिक तौर पर C का int 32-bit होता है
      16-bit target पर नहीं, लेकिन क्या आप सचमुच 5MB के program को 16-bit पर port करने वाले हैं? ऐसी चिंता आमतौर पर worthwhile नहीं होती
      समस्या long है। कुछ machines पर यह 32-bit है, कुछ पर 64-bit, इसलिए भ्रम पैदा करता है। शुक्र है कि long long हमेशा 64-bit होता है, इसलिए long को छोड़ देना चाहिए
      char 8-bit, short 16-bit, int 32-bit, long long 64-bit — बस। C में int के size पर हमने अंतहीन समय बर्बाद किया है
    • लेखक ने इसे अपना personal coding style बताकर सीमित किया है। सच कहूं तो standard types बहुत verbose हैं, और इस व्यक्ति ने जो संक्षिप्त सूची दी है, उसे पहले अपनाया गया होता तो अच्छा लगता
    • थोड़े मजाकिया अंदाज में कहें तो programming का काफी हिस्सा दूसरों के type system से निपटने का काम है
      C अक्सर इस्तेमाल करने वालों के लिए यहां दिए गए abbreviations परिचित हैं, और custom type system के लिहाज से यह काफी elegant है। Rust याद आता है
    • ऐसे types stdint.h में मौजूद हैं
      कई projects को यह file मेहनत से दोबारा बनाते देखना हमेशा हैरान करता है
      standard types को फिर से अपने नामों में translate करके इस्तेमाल करना readers के लिए झंझट है। पहले एक C++ project में collections, references और composite objects के लिए ढेर सारे typedef इस्तेमाल करने पर पूछा था, तो जवाब मिला कि इससे समझना आसान होता है
      बाद में मैंने उस व्यक्ति के monitor के पास typedef cheatsheet चिपकी हुई देखी
    • अपने integer types define करना resource-constrained platforms पर मायने रखता है
      अक्सर dim_t जैसे types दिखते हैं, जो use case के हिसाब से 32-bit या 64-bit हो जाते हैं। 64-bit platforms पर भी pointer compression structures में 32-bit integers अक्सर इस्तेमाल होते हैं
      उदाहरण के लिए, अगर आप खुद heap allocate करें और केवल 32-bit offsets store करें, तो 4GB से कम workloads में memory usage आधा हो जाता है, और cache locality भी बेहतर होकर performance सुधरती है
  • निजी पसंद के कारण C की स्थापित conventions को छोड़ देना थोड़ा ज़्यादा लगता है
    uint8_t या int32_t की जगह u8, i32 लिखने से कुछ अक्षर बचेंगे, लेकिन दूसरे लोग code पढ़ते समय उलझ सकते हैं
    null-terminated string की जगह custom string type इस्तेमाल करना भी, इस बात को देखते हुए कि C ऐसी strings के इर्द-गिर्द बनी है, collaboration को कठिन बनाने जैसा लगता है
    windows.h include किए बिना Win32 API prototypes सीधे लिखना compile time घटा सकता है, लेकिन यह अच्छी तरह बनी तेज़ highway छोड़कर जंगल के रास्ते जाने जैसा है। इनमें से काफ़ी कुछ ऐसे C code की बजाय निजी पसंद के करीब लगता है जिसे हर कोई आसानी से संभाल सके

    • u8 या i32 typing बचाने के लिए नहीं, बल्कि पढ़ते समय होने वाला संवेदी बोझ घटाने के लिए हैं
      verbosity बनाम conciseness की बहस में हमेशा आने वाला “keystrokes की संख्या” वाला तर्क काफ़ी flawed है। यह मानना गलत है कि conciseness सिर्फ़ तेज़ typing के लिए अच्छी है, और verbosity पढ़ने के लिए हमेशा बेहतर है
      verbosity के reading में फायदे हैं, लेकिन conciseness के भी फायदे हैं, और कोई भी स्पष्ट winner नहीं है। ये बस अलग-अलग trade-offs हैं
    • u16 जैसे नाम बहुत इस्तेमाल होते हैं, और programmer को confuse करने की संभावना कम है
      असली टूटने वाली जगह तब है जब दो अलग-अलग programs अपनी-अपनी तरह से u16 define करके header file में expose करें, और तीसरा program दोनों headers को एक साथ include करे
      namespace वाले library types libname_u32 जैसे दिखने लगते हैं, और उस मुकाम पर libname_ prefix की जगह सीधे uint32_t इस्तेमाल करने का मन करता है
    • किसी सक्षम C programmer के u8 या i32 देखकर confused होने की संभावना ज़्यादा से ज़्यादा theoretical है, और कुछ हद तक straw man argument जैसी लगती है
      उन्हें चिढ़ हो सकती है, लेकिन confusion नहीं होगा। Rich Hickey ने जैसा कहा था, पढ़ना सीखने से पहले हर चीज़ पढ़ने में कठिन होती है
  • कहा जाता है कि 32-bit boolean इस्तेमाल करना beginners को memory waste जैसा लग सकता है, तो शायद मैं भी beginner ही हूँ
    8-bit bool से खराब न होने के कुछ cases सुने हैं, लेकिन सच में बेहतर होने वाले cases नहीं दिखते। अगर struct में adjacent booleans हों, या function का boolean variable register से बाहर होकर stack पर रखा जाए, तब भी memory waste होती है
    कुछ bytes ही सही, लेकिन जानबूझकर pessimization क्यों करें, समझ नहीं आता। बड़ा size इस्तेमाल करके मिलता क्या है?

    • यह पूरी तरह architecture और CPU पर निर्भर है, लेकिन पुराने अनुभव में एक साफ़ case numerical processing workload था
      cycle-wise struct के आगे condition value थी और पीछे 512, 1024, 2048 sample values आते थे; एक junior ने space बचाने के लिए struct को pack कर दिया और condition value को 8-bit 1-byte बना दिया
      उस improved code ने Intel chips पर throughput लगभग 10x घटा दिया, और SPARC RISC architecture पर BUS ERROR दिया
      struct header pack करने से data array aligned नहीं रहा, और Intel को चुपचाप दो 32-bit words लाकर उन्हें जोड़ना पड़ा, जबकि SPARC ने unaligned data पर ठीक ही नाराज़गी दिखाई
      अगर यह long-term file storage नहीं बल्कि throughput-critical pipeline computation है, तो data को “space saving” के हिसाब से pack करने के बजाय architecture alignment के हिसाब से रखना बेहतर हो सकता है
    • आम तौर पर आसान optimization struct fields को 32-bit boundary पर pad करना होता है
      लगभग सभी compilers यह कर देते हैं, इसलिए “struct alignment/padding” खोजकर देख सकते हैं। अगर compiler वैसे भी खाली जगह छोड़ने वाला है, तो उस memory को खुद इस्तेमाल करना बेहतर है; नहीं तो performance छूट सकती है
      और सटीक तौर पर, हर field ऐसे address पर होना चाहिए जो उसके अपने size या wordline size से divisible हो, और पूरी struct भी सबसे बड़े field size के multiple तक padded होनी चाहिए। व्यवहार में इसका मतलब आम तौर पर 32-bit alignment होता है
      संदर्भ: http://www.catb.org/esr/structure-packing/
    • असली bool type इस्तेमाल करने पर, value false वाली 0 या true वाली 1 न हो तो sanitizer warning दे देता है
    • computer architectures ज़्यादातर चीज़ों के लिए 32-bit या उससे अधिक aligned access के लिए optimized होती हैं। आम तौर पर, हालांकि हमेशा नहीं, आपको performance मिलती है
    • मुझे जानना है कि ऐसे function का example क्या होगा जिसमें boolean variable stack पर spill हो जाए और उसके 3 bytes महत्वपूर्ण हो जाएँ
  • struct return और output parameters पर किए गए दावों से सहमत नहीं हूँ
    यह error return कर सकने वाले functions को combine करना कहीं ज़्यादा कठिन बना देता है, और जगह-जगह types बढ़ जाते हैं। असल में लगभग हर function fail हो सकता है, इसलिए खासकर अगर out-of-memory तक handle करना हो तो predictable error return style ज़्यादा महत्वपूर्ण है

    • out-of-memory handling लगभग कोई नहीं करता
      यह बहुत कठिन है और फायदा भी लगभग नहीं है। उस बिंदु पर programming style चुनने से बहुत अलग समस्याएँ पैदा हो जाती हैं
    • अगर meaningful struct unpacking संभव होती, तो सामान्य errno और output parameter semantics मिल सकते थे, लेकिन C में ऐसा नहीं है
      फिर भी C में optional values को compose करना हमेशा थोड़ा दर्दनाक रहा है। exceptions न इस्तेमाल करें तो languages में exceptions और monads दो बड़े विकल्प जैसे लगते हैं, लेकिन दोनों C में फिट नहीं बैठते और अधिकतर C programmers की philosophy से भी मेल नहीं खाते
      simple one-to-one calls के लिए macros आज़मा सकते हैं, लेकिन उनकी limits हैं। C++ भले ही भयानक हो, फिर भी C++ optional को if(foo(x,y, out1, out2) != WHATEVER_LIBRARY_OK) { ... } से इस्तेमाल करना ज़्यादा सुखद है
    • option या sum type return करना सही है, लेकिन C में इसे लिखना सच में बहुत cumbersome है
      हर function call पर if (thing(...)) goto fail लगाने वाला pattern भी बहुत शानदार नहीं लगता, लेकिन Go वालों को शायद पसंद है
      या फिर thread_local mylibrary_errno है, जो library के अंदर वास्तव में सही तरीका हो सकता है और boundary पर enum return value में convert किया जा सकता है
  • “signed sizes are the way” पर मुझे लगता है कि बस यहीं तक पढ़ना काफी था
    signed size एक बेहद चौंकाने वाला abstraction leak है और मुसीबत को बुलावा देने वाला तरीका है
    यह बात भी मानना मुश्किल है कि const की कोई व्यावहारिक भूमिका नहीं है और उसने कभी गलतियां नहीं पकड़ीं। लोग input buffer और output buffer को अक्सर गड़बड़ा देते हैं, और const इसे तुरंत सामने ला देता है
    entry point के अलावा सभी functions को static रखने की सलाह भी debugging के समय variables या functions न मिल पाने पर लेखक को कोसने की नौबत ला सकती है
    struct return को प्राथमिकता देना गलती से stack pointer return कर के बड़ा security hole खोलने के लिए अनुकूल है। output buffer पास करने पर ownership semantics साफ हो जाते हैं
    यह सलाह मुख्यतः 64-bit system code लिखने वालों के लिए किसी हद तक ठीक हो सकती है, लेकिन 32-bit embedded क्षेत्र में यह जल्दी परेशानी पैदा कर सकती है

    • Bjarne Stroustrup ने signed size के समर्थन में एक विस्तृत memo लिखा था
      https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p14...
    • जिन्हें const पसंद नहीं है वे कभी संतुष्ट नहीं होंगे, इसलिए जहां जरूरत हो वहां const इस्तेमाल करें, जितना जरूरी हो उतना propagate करें, और शिकायतों को नजरअंदाज करें
      वे हटाएं तो फिर से डाल दें। हमेशा वही पक्ष पहले थकता है। 25 साल से यही करता आया हूं और अभी भी यहां हूं
      static tools पर निर्भर कर सकता है। करीब 15 साल पहले default static और हर जगह size_t इस्तेमाल करने पर गया था, और अभी तक कोई समस्या नहीं आई
  • दिलचस्प है कि मेरा अनुभव दूसरी दिशा में गया
    https://dlang.org/blog/2023/10/02/crafting-self-evident-code...
    लेख D पर केंद्रित है, लेकिन principles C पर भी लागू होते हैं

    • पढ़ने में मजेदार था
      condition expression को doX() और doZ() के अंदर ले जाने वाला हिस्सा रोचक लगा। यह हमेशा सही है या नहीं, पता नहीं; यह इस पर निर्भर करता है कि abstraction कहां रखी गई है और code को लेकर mental model क्या है
      उदाहरण के लिए deleteRecords();, if let x = deadRecords() deleteRecords(x); से बेहतर नहीं है। दूसरा ज्यादा अस्त-व्यस्त दिखता है, लेकिन सामने ही यह दिखाने की value है कि यह delete नहीं, pruning है
      function को समझदारी से pruneDeadProjects() जैसा बदल दें तो ठीक है, लेकिन सिर्फ condition को function के अंदर ले जाना context को खतरनाक बना सकता है और leaking abstraction बन सकता है
  • सभी structs के लिए typedef इस्तेमाल करना संक्षिप्तता में मदद करता है, इसलिए मैं इसके पक्ष में हूं
    मुझे लगता है typedef उदारता से इस्तेमाल किया जा सकता है। हालांकि सिर्फ target को ही typedef करना चाहिए, pointer को नहीं। pointer चाहिए तो कभी भी (type *) लिखा जा सकता है
    खासकर function pointer के मामले में, function pointer नहीं बल्कि function itself को typedef करना चाहिए। तब function declaration में भी उस typedef का इस्तेमाल कर parameter type checking मिल सकती है, और function signature बदलते समय सभी declarations ठीक करने की जरूरत नहीं रहती
    ज्यादातर C codebases इसमें गलती करते हैं और function pointer को typedef करते हैं, और फिर भी उस pointer definition से मेल खाते function declarations हाथ से लिखने पड़ते हैं
    struct को return type की तरह इस्तेमाल करने वाले तरीके से मैं अभी आश्वस्त नहीं हूं। numeric error code को return value के रूप में रखना, और बाकी return values को output parameters से लेना मुझे ज्यादा पसंद है

    • opaque structs के लिए typedef इस्तेमाल कर सभी fields private वाली class की नकल करना, और simple data structures के लिए struct इस्तेमाल करना मुझे पसंद है
      class तक सिर्फ functions से access होना चाहिए, और struct को directly access किया जा सकना चाहिए
      यह मोटे तौर पर C/POSIX standard convention के करीब है। उदाहरण के लिए pthread_t और struct stat का फर्क यही है
    • pointer itself को typedef न करने वाली बात से सहमत हूं
      SDL_net जैसी चीजें ठीक यही करती हैं, इसलिए पसंद नहीं हैं। असल में pointer होता है, लेकिन value type जैसा typedef कर दिया जाता है
      इरादा समझ आता है, लेकिन यह काफी असहज तरीका है
  • इस लेख की कई बातें समझ में आती हैं
    Arm64 के लिए bare-metal OS लिखना शुरू किया है, और अभी शुरुआती चरण में हूं, लेकिन मिलती-जुलती चीजें कर रहा हूं। Pascal strings इस्तेमाल कर रहा हूं, और type names भी बदले हैं। बस i8 नहीं बल्कि int8 style है
    मैंने जल्दी ही तय कर लिया कि real software port करने का इरादा नहीं है, इसलिए standard C library functions या conventions का पालन करने की जरूरत नहीं है। इससे ज्यादा आजादी से experiment कर सकता हूं
    C इतनी पुरानी language है कि जिस दौर में हर byte कीमती था उसका बोझ function names तक में बचा हुआ है। उससे बाहर निकलना अच्छा लगता है, और इस लेख की बातें और कई छोटे naming changes काफी साफ-सुथरी व्यवस्था जैसे लगते हैं

    • standard C library functions या conventions का पालन करने की जरूरत नहीं है, इसका मतलब क्या यह है कि यह बस hobby project है और gnu जैसा बड़ा और professional नहीं बन सकता?
    • symbols में भी bytes बचाने के बारे में मैंने कभी सोचा नहीं था
  • typedef float f32;, typedef double f64; यह मान लेना कि float 32-बिट है और double 64-बिट, एक खतरनाक सहारा जैसा लगता है
    OpenCV float16_t को define करता है, CUDA half-precision floating point को implement करता है, और microcontroller अपनी-अपनी तरह से implement कर सकते हैं
    C++23 fixed-width floating-point types लाता है, लेकिन C में इसे enforce करने का तरीका मुझे नहीं पता। compile time पर data loss न हो रहा हो, यह check करने के लिए macro रखना बेहतर लगता है
    कुल मिलाकर, जैसा दूसरों ने कहा है, संक्षिप्त न भी हो तो readability के लिए कुछ चीज़ों को default ही रहने देना बेहतर हो सकता है
    [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