[HN Gopher] Brent's Encapsulated C Programming Rules (2020)
       ___________________________________________________________________
        
       Brent's Encapsulated C Programming Rules (2020)
        
       Author : p2detar
       Score  : 66 points
       Date   : 2025-12-09 11:16 UTC (11 hours ago)
        
 (HTM) web link (retroscience.net)
 (TXT) w3m dump (retroscience.net)
        
       | jjgreen wrote:
       | Outstanding, why hadn't I come across this before?
        
       | Jean-Papoulos wrote:
       | >What this means is that you can explain all the intent of your
       | code through the header file and the developer who uses your
       | lib/code never has to look at the actual implementations of the
       | code.
       | 
       | I hate this. If my intellisense isn't providing sufficient info
       | (generated from doc comments), then I need to go look at the
       | implementation. This just adds burden.
       | 
       | Headers are unequivocally a bad design choice, and this is why
       | most of every language past the nineties got rid of them.
        
         | GhosT078 wrote:
         | Look to Ada for "headers" (i.e. specs) done right.
        
           | runlaszlorun wrote:
           | Recently became big Ada fanboy, ironic because Im far more a
           | fan of minimal, succinct syntax like lisp, forth, etc and I
           | actually successfully lobbied a professor in 1993 to _not_
           | use it in an undergrad software engineering class lol.
           | 
           | Still in the honeymoon phase granted, but I'm actually
           | terrified that we have these new defense tech startups have
           | no clue about Ada collectively.
           | 
           | Your startup MVP you wants to ship a SaaS product ASAP and
           | iterate? Sure, grab Python or JS and whatever shitstorm of
           | libraries you want to wrestle with.
           | 
           | Want to play God and write code that kills?
           | 
           | Total category error.
           | 
           | The fact that I'm sure there are at least a few of these
           | defense tech startups yolo'ing our future away with vibe
           | coded commits when writing code that... let's not mince our
           | words... takes human life... prob says about how far we've
           | fallen from "engineering".
        
         | alextingle wrote:
         | Separating interface from implementation of one of the core
         | practices for making large code bases tractable.
        
           | valleyer wrote:
           | Of course, but that's doable without making programmers
           | maintain headers, and some modern languages do that.
        
             | ux266478 wrote:
             | I've found usually to poor effect. Both Rust and Haskell
             | did away with .mli files and ended up worse for it. Haskell
             | simplified the boundary between modules and offloaded the
             | abstractions it was used for into its more robust type
             | system, but it ended up lobotomizing the modularity
             | paradigm that ML did so well.
             | 
             | Rust did the exact opposite and spread the interface
             | language across keywords in source files, making it
             | simultaneously more complicated and ultimately less
             | powerful since it ends up lacking a cognate to higher order
             | modules. Now the interfaces are machine generated, but the
             | modularity paradigm ends up lobotomized all the same, and
             | my code is littered with pub impls (pimples, as I like to
             | call it) as though it's Java or C# or some other ugly
             | mudball.
             | 
             | For Haskell, the type system at least copes for the bad
             | module system outside of compile times. For Rust, it's hard
             | for me to say anything positive about its approach to
             | interfaces and modules. I'd rather just have .mlis instead
             | of either.
        
         | antonvs wrote:
         | C's text preprocessor headers were a pragmatic design choice in
         | the 1970s. It's just that the language stuck around longer than
         | it deserved to.
        
           | tomcam wrote:
           | So what language is ready to take its place in the thousands
           | of new chips that emerge every year, the new operating
           | systems, and millions of programs written in see every year?
        
       | unwind wrote:
       | Quite interesting, and felt fairly "modern" (which for C
       | programming advice sometimes only means it's post-2000 or so). A
       | few comments:
       | 
       | ----
       | 
       | This:                   struct Vec3* v = malloc(sizeof(struct
       | Vec3));
       | 
       | is better written as:                   struct Vec3 * const v =
       | malloc(sizeof *v);
       | 
       | The `const` is perhaps over-doing it, but it makes it clear that
       | "for the rest of this scope, the value of this pointer won't
       | change" which I think is good for readability. The main point is
       | "locking" the size to the size of the type being pointed at,
       | rather than "freely" using `sizeof` the type name. If the type
       | name later changes, or `Vec4` is added and code is copy-pasted,
       | this lessens the risk of allocating the wrong amount and is less
       | complicated.
       | 
       | ----
       | 
       | This is maybe language-lawyering, but you can't write a function
       | named `strclone()` unless you are a C standard library
       | implementor. All functions whose names begin with "str" followed
       | by a lower-case letter are reserved [1].
       | 
       | ----
       | 
       | This `for` loop header (from the "Use utf8 strings" section:
       | for (size_t i = 0; *str != 0; ++len)
       | 
       | is just atrocious. If you're not going to use `i`, you don't need
       | a `for` loop to introduce it. Either delete (`for(; ...` is
       | valid) or use a `while` instead.
       | 
       | ----
       | 
       | In the "Zero Your Structs" section, it sounds as if the author
       | recommends setting the bits of structures to all zero in order to
       | make sure any pointer members are `NULL`. This is dangerous,
       | since C does not guarantee that `NULL` is equivalent to all-bits-
       | zero. I'm sure it's moot on modern platforms where
       | implementations have chosen to represent `NULL` as all-bits-zero,
       | but that should at least be made clear.
       | 
       | [1]:
       | https://www.gnu.org/software/libc/manual/html_node/Reserved-...
        
         | jandrese wrote:
         | This:              struct Vec3* v = malloc(sizeof(struct
         | Vec3));              is better written as:              struct
         | Vec3 * const v = malloc(sizeof *v);
         | 
         | I don't love this. Other people are going to think you're only
         | allocating a pointer. It's potentially confusing.
        
           | f1shy wrote:
           | I also personally find totally confusing leaving the * in the
           | middle of nowhere, like flapping in the breeze.
        
             | unwind wrote:
             | Where would you put it? The const of the pointer is not the
             | main point, it's just extra clarity that the allocated
             | pointer is not as easily overwritten which would leak the
             | memory.
        
           | unwind wrote:
           | Uh, okay, but if you need to constantly write code as if
           | people reading it don't understand the language, then ... I
           | don't know how to do that. :)
           | 
           | It's not possible to know C code and think that
           | sizeof *v
           | 
           | and                   sizeof v
           | 
           | somehow mean the same thing, at least not to me.
        
             | ux266478 wrote:
             | no, but you can misread the two interchangeably no matter
             | how familiar you are with the language.
        
       | writebetterc wrote:
       | void* is basically used for ad-hoc polymorphism in C, and it is a
       | vital part of C programming.                   void
       | new_thread(void (*run)(void*), void* context);
       | 
       | ^- This let's us pass arbitrary starting data to a new thread.
       | 
       | I don't know whether this counts as "very few use cases".
       | 
       | The Memory Ownership advice is maybe good, but why are you
       | allocating in the copy routine if the caller is responsible for
       | freeing it, anyway? This dependency on the global allocator
       | creates an unnecessarily inflexible program design. I also don't
       | get how the caller is supposed to know how to free the memory.
       | What if the data structure is more complex, such as a binary
       | tree?
       | 
       | It's preferable to have the caller allocate the memory.
       | void insert(BinTree *tree, int key, BinTreeNode *node);
       | 
       | ^- this is preferable to the variant where it takes the value as
       | the third parameter. Of course, an intrusive variant is probably
       | the best.
       | 
       | If you need to allocate for your own needs, then allow the user
       | to pass in an allocator pointer (I guessed on function pointer
       | syntax):                   struct allocator { void* (*new)(size_t
       | size, size_t alignment); void (*free)(void* p, size_t size);
       | void* context; }.*
        
         | warmwaffles wrote:
         | Curious about the allocator, why pass a size when freeing?
        
           | naasking wrote:
           | If you don't pass the size, the allocation subsystem has to
           | track the size somehow, typically by either storing the size
           | in a header or partitioning space into fixed-size buckets and
           | doing address arithmetic. This makes the runtime more
           | complex, and often requires more runtime storage space.
           | 
           | If your API instead accepts a size parameter, you can ignore
           | it and still use these approaches, but it also opens up other
           | possibilities that require less complexity and runtime space
           | by relying on the client to provide this information.
        
             | warmwaffles wrote:
             | The way I've implemented it now was indeed to track the
             | size in a small header above the allocation, but this was
             | only present in debug mode. I only deal with simple
             | allocators like a linear, pool, and normal heap allocator.
             | I haven't found the need for something super complex yet.
        
         | mrkeen wrote:
         | void* is a problem because the caller and callee need to
         | coordinate across the encapsulation boundary, thus breaking it.
         | (Internally it would be fine to use - the author could
         | carefully check that qsort casts to the right type inside the
         | .c file)
         | 
         | > What if the data structure is more complex, such as a binary
         | tree?
         | 
         | I think that's what the author was going with by exposing
         | opaque structs with _new() and _free() methods.
         | 
         | But yeah, his good and bad versions of strclone look more or
         | less the same to me.
        
       | fpotier wrote:
       | void employee_set_age(struct Employee* employee, int newAge) { //
       | Cast away the const and set it's value, the compiler should
       | optimize this for you _(int_ )&employee->age = newAge; }
       | 
       | I believe that "Casting away the const" is UB [1]
       | 
       | [1]: https://en.cppreference.com/w/c/language/const.html
        
         | spacechild1 wrote:
         | It's only UB if the pointed to object is actually const (in
         | which case it might live in read-only memory).
        
       | zoomablemind wrote:
       | _"...C is my favorite language and I love the freedom and
       | exploration it allows me. I also love that it is so close to
       | Assembly and I love writing assembly for much of the same
       | reasons! "_
       | 
       | I wonder what is author's view about user's reasons to choose a C
       | API?
       | 
       | What I mean is users may want exactly the same freedom and
       | immediacy of C that the author embraces. However, the very
       | approach to encapsulation by hiding the layout of the memory, the
       | use of accessor functions limits the user's freedom and robs them
       | of performance too.
       | 
       | In my view, the choice of using C in projects comes with certain
       | responsibilities and expectations from the user. Thus higher
       | degree of trust to the API user is due.
        
       | pizlonator wrote:
       | Good stuff.
       | 
       | Only things I disagree with:
       | 
       | - The out-parameter of strclone. How annoying! I don't think this
       | adds information. Just return a pointer, man. (And instead of
       | defending against the possibility that someone is doing some
       | weird string pooling, how about jut disallow that - malloc and
       | free are your friends.)
       | 
       | - Avoiding void _. As mentioned in another comment, it 's useful
       | for polymorphism. You can do quite nice polymorphic code in C and
       | then you end up using void_ a lot.
        
         | syncsynchalt wrote:
         | Yes that section raised my hackles too, to the point where I'm
         | suspicious of the whole article.
         | 
         | The solution, in my opinion, is to either document that
         | strclone()'s return should be free()'d, or alternately add a
         | strfree() declaration to the header (which might just be
         | `#define strfree(x) free(x)`).
         | 
         | Adding a `char **out` arg does not, in my opinion, document
         | that the pointer should be free()'d.
        
       | breckinloggins wrote:
       | Other resources I like:
       | 
       | - Eskil Steenberg's "How I program C"
       | (https://youtu.be/443UNeGrFoM). Long and definitely a bit
       | controversial in parts, but I find myself agreeing with most of
       | it.
       | 
       | - CoreFoundation's create rule (https://stackoverflow.com/questio
       | ns/5718415/corefoundation-o...). I'm definitely biased but I
       | strongly prefer this to OP's "you declare it you free it" rule.
        
         | writebetterc wrote:
         | I can't edit my comment any longer, but I really like
         | nullprogram.com
        
           | capyba wrote:
           | Same here! That's a great blog with a lot of good advice.
        
         | quelsolaar wrote:
         | Thanks for the shout out. I had no idea my 2h video, without a
         | camera 8 years ago would have such legs! I should make a new
         | one and include why zero initialization is bad.
        
           | elcapitan wrote:
           | Thank you for recording it! :) It hits the right balance
           | between opinionated choices with explanations and a general
           | introduction to "post-beginner" problems which probably a lot
           | of people who have programming experience, but not in C,
           | face.
        
       | syockit wrote:
       | Check against FLT_EPSILON. Oh boy.
       | 
       | The reason is floating point precision errors, sure, but that
       | check is not going to solve the problems.
       | 
       | Took a difference of two numbers with large exponents, where the
       | result should be algebraically zero but isn't quite numerically?
       | Then this check fails to catch it. Took another difference of two
       | numbers with very small exponents, where the result is not
       | actually algebraically zero? This check says it's zero.
        
         | syncsynchalt wrote:
         | Yeah, at the least you'll need an understanding of ULPs[0]
         | before you can write code that's safe in this way. And
         | understanding ULPs means understanding that no single constant
         | is going to be applicable across the FLT or DBL range.
         | 
         | [0] https://en.wikipedia.org/wiki/Unit_in_the_last_place
        
       | f1shy wrote:
       | > Make sure that you turn on warnings as errors
       | 
       | I'm seeing this way too often. It is a good idea to never ignore
       | a warning, an developers without discipline may need it. But for
       | god's sake, there is a reason why there are warnings and errors
       | ,and they are treated differently. I don't think compiler writers
       | and/or C standards will deprecate warnings and make them errors
       | anytime soon, and for good reason. So IMHO is better to treat
       | errors as errors and warnings as warnings. I have seen plenty of
       | times this flag is mandatory, and to avoid the warning (error)
       | the code is decorated with compiler pacifiers, which makes no
       | sense!
       | 
       | So for some setups I understand the value, but doing it all the
       | time shows some kind of lazyness.
        
         | Chabsff wrote:
         | > and to avoid the warning (error) the code is decorated with
         | compiler pacifiers, which makes no sense!
         | 
         | How is that a bad thing, exactly?
         | 
         | Think of it this way: The pacifiers don't just prevent the
         | warnings. They embed the warnings within the code itself in a
         | way where they are acknowledged by the developer.
         | 
         | Sure, just throwing in compiler pacifiers willy-nilly to
         | squelch the warnings is _terrible_.
         | 
         | However, making developers explicitly write in the code "Yes,
         | this block of code triggers a warning, and yes it's what I want
         | to do because xyz" seems not only perfectly fine, but straight
         | up desirable. Preventing them from pushing the code to the repo
         | before doing so by enabling warnings-as-errors is a great way
         | to get that done.
         | 
         | The only place where I've seen warnings-as-errors become a huge
         | pain is when dealing with multiple platforms and multiple
         | compilers that have different settings. This was a big issue in
         | Gen7 game dev because getting the PS3's gcc, the Wii's
         | CodeWarrior and the XBox360's MSVC to align on warnings was
         | like herding cats, and not every dev had every devkit for
         | obvious reason. And even then, warnings as errors was still
         | very much worth it in the long run.
        
       ___________________________________________________________________
       (page generated 2025-12-09 23:01 UTC)