7 ms·
Some C habits I employ for the modern day
- skywalqer 9mo agoNice post, but the flashy thing on the side is pretty distracting. I liked the tuples and maybes.
- smnplk 9mo agoNot distracting at all, it feels nostalgic to me. Id rather have these flashy things than a million popups and registration forms following you around, which is basically the modern web. I hate it so much. This site is pure balsam for my soul.
- Vedor 9mo agoBoth nostalgic and distracting for me.
- matheusmoreira 9mo ago> In the absence of proper language support, “sum types” are just structs with discipline. With enough compiler support they could be more than that. For example, I submitted a tagged union analysis feature request to gcc and clang, and someone generalized it into a guard builtin. https://github.com/llvm/llvm-project/issues/74205 https://github.com/llvm/llvm-project/issues/74205 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=112840 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=112840 GCC proved to be too complex for me to hack this in though. To this day I'm hoping someone better than me will implement it.
- nine_k 9mo agoWith proper discipline, one can even program a Turing machine directly. The problems are two: (1) Doing so is very slow and arduous, and (2) a chance of making a dangerous error is still quite high. For instance, it appears that no amount of proper discipline, even in the best developers, allows to replace proper array support with a naked pointer to a memory area.
- matheusmoreira 9mo agoThe compiler's job is to program the turing machine for us. It should help as much as possible. For example, I really like using enums because compilers have extensive support for checking that all values have been handled in switch statements. I don't like it when compilers start getting in the way though. We use C because we want to do raw things like point a structure at some memory area in order to access the data stored there. The compiler's job is to generate the expected code without screwing it up by "optimizing" it beyond recognition because of strict aliasing or some other nonsense.
- convolvatron 9mo agoyou can certainly wrap the array with a structure which provides either bounds information to be checked with generic runtime functions, or specific function pointers (methods) to get and set. you can paper over _alot_ of Cs faults. ultimately its not really worth it, but its not nearly as fragile and arduous as you make it out to be
- adrianN 9mo agoYou can do such things until you have to interface with other code, eg the operating system.
- convolvatron 9mo agoSo that’s an interesting case. I’d really like to keep language neutrality, because I don’t think we’re finished evolving yet. So this is a place where we need an abi. The first things we try to do is be simple…except for a terrible mistake with select, we don’t send arrays across that interface, sadly, we send c structs sometimes and I think that’s pretty horrible, because we have to try to lay them out in a compatible way, which is pretty fragile. The other sad bit is that we need to verify the addresses before we can operate on them, and that’s hugely prone to error. Im curious if you have a suggestion about how to fix both of those. The structure thing can clearly be a more robust serialization. Addresses? Idk
- apaprocki 9mo agoFWIW, Coverity (maybe others) has a checker that creates an error if it detects tagged union access without first checking the tag. It’s not as strict as enforcing which fields belong to which tag values, but it can still be useful. I’d much rather have what was proposed in the GCC bug!
- uecker 9mo agoIt is on my list (also as a proposal to WG14). Sorry, I am a bit too overloaded currently. (If people want to help with such improvements - with either time or money, let me know.).
- canpan 9mo agoRegarding memory, I recently changed to try to not use dynamic memory, or if I need to, to do it once at startup. Often static memory on startup is sufficient. Instead use the stack much more and have a limit on how much data the program can handle fixed on startup. It adds the need to think what happens if your system runs out of memory. Like OP said, it's not a solution for all types of programs. But it makes for very stable software with known and easily tested error states. Also adds a bit of fun in figuring out how to do it.
- vbezhenar 9mo agoIn recent years I had to write some firmware code with C and that was exactly the approach I took. So far I never had need for any dynamic memory and I was surprised how far I can get without it.
- thisoneisreal 9mo agoI've been looking into Ada recently and it has cool safety mechanisms to encourage this same kind of thing. It even allows you to dynamically allocate on the stack for many cases.
- apaprocki 9mo agoYou can allocate dynamically on the stack in C as well. Every compiler will give you some form of alloca().
- adrian_b 9mo agoTrue, but in many environments where C is used the stacks may be configured with small sizes and without the possibility of being grown dynamically. In such environments, it may be needed to estimate the maximum stack usage and configure big enough stacks, if possible. Having to estimate maximum memory usage is the same constraint when allocating a static array as a work area, then using a custom allocator to provide memory when needed.
- 9mo ago
- sys_64738 9mo ago#define BEGIN { #define END } /* scream! */
- JamesTRexx 9mo agoTwo things I thought while reading the post: Why not typedef BitInt types for stricter size and accidental promotion control when typedeffing for easier names anyway? I came across a post mentioning using regular arrays instead of strings to avoid the null terminatorand off-by-one pitfalls. I still have a lot of conversion to do before I can try this in my hobby project, but these are interesting ideas.
- jcalvinowens 9mo ago#if CHAR_BIT != 8 #error "CHAR_BIT != 8" #endif In modern C you can use static_assert to make this a bit nicer. static_assert(CHAR_BIT == 8, "CHAR_BIT is not 8"); ...although it would be a bit of a shame IMHO to add that reflexively in code that doesn't necessarily require it. https://en.cppreference.com/w/c/language/_Static_assert.html https://en.cppreference.com/w/c/language/_Static_assert.html
- gdjjg 9mo agoGtav
- procaryote 9mo agoEven if the code might not end up requiring it, if you write it with the assumption that bytes are 8 bits, it's good to document that with a static assert so someone porting things knows there will be dragons It's a pretty neat way to drop some corner cases from your mental load without building subtle traps
- jcalvinowens 9mo agoThat's pretty silly IMHO, it should be incredibly obvious to anybody who is ever in a position to port code to a machine with non-8-bit-bytes that there will be dragons there. It also requires including limit.h which you might not otherwise need. It's just not a realistic edge case, the machines like this are either antiquated or are tiny microcontrollers that can't practically run a POSIX OS. Very little code in the real world is generic enough to be useful in that environment (a good example might be a fixed point signal processing library). There is no assertion in the entire Linux kernel that CHAR_BIT is eight, despite that assumption being hardcoded in many places.
- WalterBright 9mo ago> I’ve long been employing the length+data string struct. If there was one thing I could go back and time to change about the C language, it would be removal of the null-terminated string. It's not necessary to go back in time. I proposed a way to do it in modern C - no existing code would break: https://www.digitalmars.com/articles/C-biggest-mistake.html https://www.digitalmars.com/articles/C-biggest-mistake.html It's simple, and easy to implement.
- publicdebates 9mo ago> the fatal error was not combining the array dimension with the array pointer; all it needs is a little new syntax a[...]; this won’t fix any existing code. Over time, the syntax a[] can be deprecated by convention and by compilers. You're thinking in decades. C standard committee is slower than that. This could have worked in practice, but probably never will happen in practice. Maybe people should start considering a language like D[1] as an alternative, which seems to have the spirit of both C and Go, but with much more pragmatism than either. [1] https://en.wikipedia.org/wiki/D_(programming_language)#Critique https://en.wikipedia.org/wiki/D_(programming_language)#Criti...
- billforsternz 9mo agoThere is some irony in someone replying to the author of the D language suggesting that maybe the D language is the real solution he's looking for.
- I_am_uncreative 9mo agoA tale as old as time.
- publicdebates 9mo agoIt might be the language he is looking for, but it might not, and more likely than not is not. D is one of those odd languages which most likely ought to have gotten a lot more popular than it did, but for one reason or another, never quite caught on. Perhaps one reason is because it lacks a sense of eccentricity and novelty that other languages in its weight class have. Or perhaps it's just too unfamiliar in all the wrong ways. Whatever the case may be, popularity is in fact one of the most useful metrics when ruling out a potential language for a new project. And if D does not meet GP's requirements in terms of longevity or commercial support, I would certainly not suggest GP adopt it too eagerly, simply because it happens to check off most or all their technological requirements.
- BigJono 9mo agoI really dislike parsing not validating as general advice. IMO this is the true differentiator of type systems that most people should be familiar with instead of "dynamic vs static" or "strong vs weak". Adding complexity to your type system and to the representation of types within your code has a cost in terms of mental overhead. It's become trendy to have this mental model where the cost of "type safety" is paid in keystrokes but pays for itself in reducing mental overhead for the developers. But in reality you're trading one kind of mental overhead for another, the cost you pay to implement it is extra. It's like "what are all the ways I could use this wrong" vs "what are all the possibilities that exist". There's no difference in mental overhead between between having one tool you can use in 500 ways or 500 tools you can use in 1 way, either way you need to know 500 things, so the difference lies elsewhere. The effort and keystrokes that you use to add type safety can only ever increase the complexity of your project. If you're going to pay for it, that complexity has to be worth it. Every single project should be making a conscious decision about this on day one. For the cost to be worth it, the rate of iteration has to be low enough and the cost of runtime bugs has to be high enough. Paying the cost is a no brainer on a banking system, spacecraft or low level library depended on by a million developers. Where I think we've lost the plot is that NOT paying the cost should be a no brainer for stuff like front end web development and video games where there's basically zero cost in small bugs. Typescript is a huge fuck up on the front end, and C++ is a 30 year fuck up in the games industry. Javascript and C have problems and aren't the right languages for those respective jobs, but we completely missed the point of why they got popular and didn't learn anything from it, and we haven't created the right languages yet for either of those two fields. Same concept and cost/benefit analysis applies to all forms of testing, and formal verification too.
- lelanthran 9mo agoWhile I broadly agree with your general point, in that engineering is making a set of trade-offs, I don't necessarily agree that ditching type-safety in the example contexts you posted is the appropriate trade-off.[1] I'll ditch type-safety in experimental/exploratory code; I'll use Lisp (or, more recently, Python) to test if something is a good idea. For anything that ships to production, I think a basic level of type enforcement is necessary, even if you don't want the whole type zoo. For your Javascript f/end context, I like the proposed TC39 approach (https://github.com/tc39/proposal-type-annotations?tab=readme-ov-file#proposal https://github.com/tc39/proposal-type-annotations?tab=readme...). The typing is optional, does not break existing syntax and can still be used to enforce a basic level of type safety if the developer wants it. ---------------------------- [1] I upvoted you anyway. Your broader point is still valid.
- tom_ 9mo agoIf you really insist on not having a distinction between "u8"/"i8" and "unsigned char"/"signed char", and you've gone to the trouble of refusing to accept CHAR_BIT!=8, I'm pretty sure it'd be safer to typedef unsigned char u8 and typedef signed char i8. uint8_t/int8_t are not necessarily character types (see 6.2.5.20 and 7.22.1.1) and there are ramifications (see, e.g., 6.2.6.1, 6.3.2.3, 6.5.1).
- _zagj 9mo ago> and you've gone to the trouble of refusing to accept CHAR_BIT!=8 This one was a head-scratcher for me. Yeah, there's no cost to check for it, but architectures where CHAR_BIT != 8 are rarer even than 24-bit architectures.
- apaprocki 9mo agoI got the impression the author was implying because CHAR_BIT is enforced to be 8 that uint8_t and char are therefore equivalent, but they are different types with very different rules. E.g. `char p = (char )&astruct` may violate strict aliasing but `uint8_t p = (uint8_t )&astruct` is guaranteed legal. Then modulo, traps, padding, overflow, promotion, etc.
- deleted 9mo ago[deleted]
- sgsjchs 9mo agoIt's the other way around.
- publicdebates 9mo agoCould you clarify an example of the ramifications? I tried looking through the C2Y standard draft to figure it out, but it's too complicated for me.
- tom_ 9mo agoWith the disclaimer that I let my language lawyer qualification lapse a while ago, it's broadly to do with the character types being the only approved way to examine the bytes of an object. An object of a type can be accessed only as if it were an object of that type or some compatible type, but: it can also be accessed as a sequence of characters. (You'd do this if implementing memcpy, memset or memcmp, for example.) 6.2.6.1 - only character types can be used to inspect the sequence of bytes making up an objuect, and (interestingly) only an array of unsigned char is suitable for memcpy'ing an object into for inspection. It's possible for sequences of bytes to exist that don't represent a valid value of the original object; it's undefined behaviour to read those sequences of bytes other than via a character type (i.e., I think, via a pointer to something compatible with the object's actual type - there being no other valid ways to even attempt to read it) 6.3.2.3 - when casting a pointer to an object type to a pointer to a character type, the new character pointer points to the bytes of the object. If converting between object types, on the other hand, the original pointer will (with care) round trip, and that seems to be all you can do, and actual access is not permitted 6.5.1 - as well as all the expected ways of accessing an object, objects can be accessed via a character pointer
- keyle 9mo agoThat made me smile If I find myself needing a bunch of dynamic memory allocations and lifetime management, I will simply start using another language–usually rust or C#. Now that is some C habit for the modern day... But huh, not C.
- pjmlp 9mo agoI started doing that in 1993 on MS-DOS already, thanks to C++ RAII, C felt outdated already on those days.
- procaryote 9mo agoArguably, 1993's C has survived better than 1993's C++.
- pjmlp 9mo agoWell in 33 years it has learnt nothing about memory safe programming, at least C++ provides the tooling for those that care, before even goverments decided to act upon it.
- krapp 9mo agoMy go to language for that is lua. I'm starting to think of it as a C framework more so than its own language.
- amiga386 9mo agoFun fact: the background image is the "BallsMany" pattern included with MagicWB for the Amiga (To confirm: download the LhA archive from https://aminet.net/package/util/wb/MagicWB21p https://aminet.net/package/util/wb/MagicWB21p then open the archive in 7-zip, extract Patterns/BallsMany then load into an ILBM viewer, e.g. https://www.retroreversing.com/ilbm https://www.retroreversing.com/ilbm )
- themafia 9mo ago> and I end up having all these typedefs in my projects I avoid doing this now. It's more trouble than it's worth and it changes your code from a standard dialect of C into a custom one. Plus my eyes are old and they don't enjoy separating short identifiers. > typedef struct { ... } String I avoid doing this. Just use `struct string { ... };'. It makes it clear what you're handling. C23 finally gave us "auto", you shouldn't fret over typedefing everything anymore. I also prefer a "strbuf" type with an index and capacity so I can safely read and write to it with a derived "strview" having pointer and length only which references into the buffer. > returning results The general method of returning structures larger than two machine words is fairly inefficient. Plus you're cutting yourself off from another C23 gem which was [[nodiscard]]. If you want the 'ok' value checked then you can _really_ specify that. Put everything else behind a pointer passed in an argument. The sum type logic works just as well there. > I tend to avoid the string.h functions most of the time, only employing the mem family when I want to, well, mess with memory. So you use strlen() a lot and don't have to deal with multibyte characters anywhere in your code. It's not much of a strategy.
- apaprocki 9mo ago> > typedef struct { ... } String > I avoid doing this. Just use `struct string { ... };'. It makes it clear what you're handling. Well then imagine if Gtk made you write `struct GtkLabel`, etc. and you saw hundreds of `struct` on the screen taking up space in heavy UI code. Sometimes abstractions are worthwhile.
- wavemode 9mo agoThe main thing I dislike about typedefs is that you can't forward declare them. If I know for sure I'm never going to need to do that then OK.
- procaryote 9mo agoHow do you mean? You can at least do things like typedef struct foo foo; and somewhere else struct foo { … }
- doanbactam 9mo agoSolid list. The bit about avoiding the preprocessor as much as possible really resonates—using `static inline` functions and `enum` instead of macros makes debugging so much less painful. What's your take on using C11's `_Generic` for type-generic macros? It adds some verbosity but can save you from a lot of runtime type errors.
- 0xbadcafebee 9mo ago> I think one of the most eye-opening blog posts I read when getting into programming initially was the evergreen parse, don’t validate post Bro, that was written in 2019. If it's not old enough to drink it's not yet evergreen. But it's also long-winded. A 25-minute read, and y'know what the conclusion is? "Parsing leaves you with a new data structure matching a type, validation checks if some data technically complies with a type (but might not later be parsed correctly)". I need all the baby programmers in the back to hear me: type systems are bikeshedding. The point of a type is only to restrict computation to a fixed set. This concept can be applied anywhere you need to ensure reliability and simplicity. You don't need a programming language to natively support types in order to implement the concept yourself in that language.
- lelanthran 9mo ago> You don't need a programming language to natively support types in order to implement the concept yourself in that language. In a programming language that doesn't enforce types, how do you implement > "Parsing leaves you with a new data structure matching a type, validation checks if some data technically complies with a type (but might not later be parsed correctly)".
- apaprocki 9mo agoPlease don’t buy into “no const”. If you’ve ever worked with a lot of C/C++ code, you really appreciate proper const usage and it’s very obvious if a prototype is written incorrectly because now any callers will have errors. No serious reusable library would expose functions taking char* without proper const usage. You would never be able to pass a C++ string c_str() to such a C function without a const_cast if that were the case. Casting away const is and should be an immediate smell.
- Panzerschrek 9mo agoYet another C person reinventing things which C++ already has.
- lelanthran 9mo ago> Yet another C person reinventing things which C++ already has. And yet another C++ person salty that people prefer simpler things.
- pjmlp 9mo agoC23 + <compiler C extensions> is hardly simpler as people advocate.
- oguz-ismail2 9mo agoI can't think of a language that isn't simpler compared to C++
- pjmlp 9mo agoMight be, then again C23 isn't K&R C that many still learn from.
- lelanthran 9mo ago> Might be, then again C23 isn't K&R C that many still learn from. I agree with this, but then again, not many people are learning C now anyway. It will die away from natural attrition anyway, is my point. The K&R C does have a few advantages, because the compilers at the time were not so aggressive in optimisation, and will consistently emit code that (for example) performed a NULL dereference (or other UB), ensuring things like consistently crashing instead of silently losing data/doing the wrong thing.
- lelanthran 9mo ago> C23 + <compiler C extensions> is hardly simpler as people advocate. Well, certainly simpler than C++, at any rate. I mean, just knowing the assignment rules in C++ is worthy of an entire book on its own. Understandably, the single rule of "assignment is a bitwise copy of the source variable into the destination variable" is inflexible, but at least the person reading the local code can, just from the current scope, determine whether some assignment is a bug or not! In many ways, C++ requires global context when reading any local scope: will the correct destructor get called? Can this variable be used as an argument to a function (a lack of a copy constructor results in the bitwise copy for on stack, with the destructor for that instance running twice - once in the stack and again when the scope ends)? Is this being passed by reference (i.e. it might be modified by the function we are calling) or by value (i.e. we don't need to worry about whether `bar` has been changed after a call to `foo(bar)`). Many programmers don't like holding lots of global scope in their head when working in some local scope. In C, all those examples above are clear in the local scope. All programmers who prefer C over C++ have already tried C++ in large and non-trivial projects before walking away. I doubt that the reverse is true.
- taminka 9mo agoreally cool website, what's your colour palette?
- moth-fuzz 9mo agoI'm a huge fan of the 'parse, don't validate' idiom, but it feels like a bit of a hurdle to use it in C - in order to really encapsulate and avoid errors, you'd need to use opaque pointers to hidden types, which requires the use of malloc (or an object pool per-type or some other scaffolding, that would get quite repetitive after a while, but I digress). You basically have to trade performance for correctness, whereas in a language like C++, that's the whole purpose of the constructor, which works for all kinds of memory: auto, static, dynamic, whatever. In C, to initialize a struct without dynamic memory, you could always do the following: struct Name { const char *name; }; int parse_name(const char *name, struct Name *ret) { if(name) { ret->name = name; return 1; } else { return 0; } } //in user code, *hopefully*... struct Name myname; parse_name("mothfuzz", &myname); But then anyone could just instantiate an invalid Name without calling the parse_name function and pass it around wherever. This is very close to 'validation' type behaviour. So to get real 'parsing' behaviour, dynamic memory is required, which is off-limits for many of the kinds of projects one would use C for in the first place. I'm very curious as to how the author resolves this, given that they say they don't use dynamic memory often. Maybe there's something I missed while reading.
- danhau 9mo ago> But then anyone could just instantiate an invalid Name without calling the parse_name function and pass it around wherever This is nothing new in C. This problem has always existed by virtue of all struct members being public. Generally, programmers know to search the header file / documentation for constructor functions, instead of doing raw struct instantiation. Don‘t underestimate how good documentation can drive correct programming choices. C++ is worse in this regard, as constructors don‘t really allow this pattern, since they can‘t return a None / false. The alternative is to throw an exception, which requires a runtime similar to malloc.
- apaprocki 9mo agoIn C++ you would have a protected constructor and related friend utility class to do the parsing, returning any error code, and constructing the thing, populating an optional, shared_ptr, whatever… don’t make constructors fallible.
- SkiFire13 9mo ago> Additionally, the intent of whether the buffer is used as “raw” memory chunks versus a meaningful u8 is pretty clear from the code that it gets used in, so I’m not worried about confusing intent with it. It's generally not clear to the compiler, and that can result in missed optimization opportunities.
- bArray 9mo ago> I don’t personally do things that require dynamic memory management in C often, so I don’t have many practices for it. I know that wellons & co. Have been really liking the arena, and I’d probably like it too if I actually used the heap often. But I don’t, so I have nothing to say. > If I find myself needing a bunch of dynamic memory allocations and lifetime management, I will simply start using another language–usually rust or C#. I'm not sure what the modern standards are, but if you are writing in C, pre-allocate as much as possible. Any kind of garbage collection is just extra processing time and ideally you don't want to run out of memory during an allocation mid-execution. People may frown at C, but nothing beats getting your inner loops into CPU cache. If you can avoid extra fetches into RAM, you can really crank some processing power. Example projects have included computer vision, servers a custom neural network - all of which had no business being so fast.
- boltzmann-brain 9mo agooh god, i find the sidebar so intensely annoying. glad i have the Dom Delete addon, i would never manage to read anything otherwise
- lelanthran 9mo ago> oh god, i find the sidebar so intensely annoying. glad i have the Dom Delete addon, i would never manage to read anything otherwise A Dom Delete addon? Is it faster than hitting f12, clicking on the offending element and hitting delete?
- boltzmann-brain 9mo agoyes
- lelele 9mo ago.