HNNewShowAskJobs
Built with Tanstack Start
How to write generics in C(raphgl.github.io)
72 points by todsacerdoti 4 days ago | 65 comments
  • QuadmasterXLII4 days ago

    Instead of invisibly generating the per type headers and implementations via macros during compilation, I generate them using something like M4 and check them into the version control system. Then, instead of integrating M4 into the compilation process, I verify that the checked in generated files match the output of m4 in the CI checks. The prettiness of the generated files is now a code quality target. This provides the best possible developer experience to anyone who is not modifying the generic containers, and survivable dx if you are modifying the generic containers, at which point you kind of deserve it and any mistakes from forgetting to regenerate and re check in are isolated from other users at the CI step.

    This is secretly two way generation: you can edit The generated file, recompiling until until the code works, commit that, and then edit the M4 inputs until CI passes.

    As another example of the same principle, whenever I am required to have a requirements.txt and setup.cfg in the same puthon library, I now verify that they match in a CI check instead of trying to single source of truth by having a setup.cfg import the requirements.txt through some clever hack.

  • lelanthran4 days ago

    Having used this in production around 2004, I never really liked this approach - abusing the preprocessor this much shouldn't be necessary and the result is almost unreadable.

    I think a better way is possible using `_Generic`[1]. Even though it would still use macros, the resulting code is much more readable.

    ---------------------------

    [1] `_Generic` comes with its own problems too, of course.

    • naasking4 days ago |parent

      > abusing the preprocessor this much shouldn't be necessary and the result is almost unreadable.

      The readability of error messages was always my beef with macro-based generics. Maybe 15 years ago I played around with using UTF characters for the brackets and type separators when translating a generic type into name-mangled type, so at least the generated source and error messages would still be clear and understandable.

      Robust UTF support for identifiers and preprocessor was still kind of inconsistent across compilers though, so it only sort of worked. I expect this has improved considerably now though, so maybe I should try again and write something up. You can embed a surprising amount of advanced programming language theory into suitable name resolution.

  • schaefer4 days ago

    A more accurate title would be:

    “How to write type safe generics using the C preprocessor”

  • kevin_thibedeau4 days ago

    I recommend m4 for doing this sort of thing. It's much easier to manipulate the generated source with its facilities than with the preprocessor.

  • pjmlp4 days ago

    Type safety, generics and C is a bit of oxymoron.

    • keyle3 days ago |parent

      Everything above is a fat pointer! /s

  • wffurr4 days ago

    Why did this get flagged? Weird.

    • abnercoimbre3 days ago |parent

      Strange indeed. I thought maybe the article was seen as AI slop, but nothing about it suggests any such thing. It's a decent blog post.

  • david2ndaccount4 days ago

    I wrote a similar article in the past: https://www.davidpriver.com/ctemplates.html

    I use this technique in my hobby projects as it has worked out the best for me of the options I’ve explored.

  • jesse__4 days ago

    I wrote a metaprogramming language that adds another option to the list, for anyone that's interested : https://github.com/scallyw4g/poof

  • mistivia3 days ago

    I used similar method to craft my own type-safe algorithm and data structure library in C: https://github.com/mistivia/algds

    Although it's a toy not for production, it's very interesting that the idea of traits/concept/typeclass just "emerge" out of the code, which made me suddenly understand why STL come into being.

  • uecker3 days ago

    Here is my take on this: https://uecker.codeberg.page/2025-07-20.html

    (you could also create a version with capacity, of course, but I like it this way).

  • tester7564 days ago

    Why they cannot add generics to C as first class citizen, so you don't have to abuse macros, preprocessing and other primitive techniques?

    • xigoi4 days ago |parent

      Because according to C programmers, generics are too “complex” (as opposed to preprocessor abuse, which is “simple”).

      • uecker3 days ago |parent

        There are too complex. And having suffered from endless hours debugging C++ template code in the past, I think C's macros are actually better - if done carefully.

        • xigoi2 days ago |parent

          Well, I guess anything is simple if you consider C++ as the only alternative.

          • uecker2 days ago |parent

            My generics in C are quite simple in my opinion: https://uecker.codeberg.page/2025-07-02.html

  • self_awareness4 days ago

    Well, another option would be to use a C++ compiler, which supports templates, but limit the use of classes through a coding convention standard.

    • krupan4 days ago |parent

      Not sure why this is down voted when the whole point of TFA is to torture the C language into doing something it can't really do. I guess there's an unspoken assumption in TFA that you are stuck using C and absolutely cannot use a different language, not even C++?

      • relling4 days ago |parent

        This looks far less tortured to my eye than C++. I think a lot of us have this self-imposed rule, for whatever reason, that we absolutely will not use C++.

    • lelanthran4 days ago |parent

      > Well, another option would be to use a C++ compiler, which supports templates, but limit the use of classes through a coding convention standard.

      When the other option is "ask the developers to practice discipline", an option that doesn't require that looks awfully attractive.

      That being said, I'm not a fan of the described method either. Maybe the article could have shown a few more uses of this from the caller perspective.

      • krupan4 days ago |parent

        "ask the developers to practice discipline" is a baseline requirement for coding in C

        • EPWN3D4 days ago |parent

          And it hasn't worked in practice. C unfortunately does not have a very big pit of success -- it's way too hard to do the right thing and way too easy to do the wrong thing.

          The solution to this doesn't have to be "rewrite everything in Rust", but it does mean that you need to provide safe, easy implementations for commonly-screwed-up patterns. Then you're not asking people to be perfect C programmers; you're just asking them to use tools that are easier than doing the wrong thing.

        • lelanthran4 days ago |parent

          > "ask the developers to practice discipline" is a baseline requirement for coding in C

          Sure, but since there's 10x more opaque footguns in C+++, there is much less discipline needed than when coding in C++.

          The footguns in C are basically signed-integer over/underflows and memory errors. The footguns in C++ include all the footguns in C, and then add a ton more around object construction type, object destruction types, unexpected sharing of values due to silent and unexpected assignments, etc.

          Just the bit on file-scope/global-scope initialisation alone can bite even experienced developers who are adding a new nonlocally-scoped instance of a new class.

        • pjmlp4 days ago |parent

          Unfortunately the majority has failed to attend the temple classes on such practices.

        • Loudergood4 days ago |parent

          If only.

    • EPWN3D4 days ago |parent

      It never seems to work out that way though. C++ is just too large a language, and it gets bigger with each revision. The minute you hire a senior/principal engineer who loves C++, they'll make the case to enable "just this one" feature, and before you know it, you've got a sprawling C++ code base, not a "C with a light dusting of C++" code base.

    • pjmlp4 days ago |parent

      C folks rather reproduce badly C++ than acknowledge its Typescript like improvements over C.

      • pron4 days ago |parent

        C is sometimes used where C++ can't be. Exotic microcontrollers and other niche computing elements sometimes only have a C compiler. Richer, more expressive languages may also have additional disadvantages, and people using simpler, less expressive languages may want to enjoy some useful features that exist in richer languages without taking on all of their disadvantages, too. Point being, while C++ certainly has some clear benefits over C, it doesn't universally dominate it.

        TS, on the other hand, is usable wherever JS is, and its disadvantages are much less pronounced.

        • pjmlp4 days ago |parent

          It isn't the 1980's any longer, there isn't a chip in such scenarios other than PIC class, even AVR get to use C++.

          • pron4 days ago |parent

            8051s are still programmed almost entirely in C. There are C++ compilers available, but they're rarely used. Even on STM32, C is more popular. There's a perception -- and not an unsubstantiated one -- that C++'s can more easily sneak in operations that could go unnoticed.

            C++ has many advantages over C, but it also brings some clear disadvantages that matter more when you want to be aware of every operation. When comparing language A against language B, it's not enough to consider what A does better than B; you also have to consider what it does worse.

            That's why I don't think that the comparison to TS/JS is apt. Some may argue that C++ has even more advantages over C than TS has over JS, but I think it's fairly obvious that its disadvantages compared C are also bigger. For all its advantages, there are some important things that C++ does worse than C. But aside from adding a build step (which is often needed, anyway), it's hard to think of important things that TS does worse than JS.

            • pjmlp3 days ago |parent

              More like Assembly, if devs are serious enough.

              If there is any C, it is hardly any different from using C as cheap macro Assembler, with lots of inline Assembly.

              Also definitely a 1980's CPU.

              It is more than apt, until C gets serious about having something comparable to std::array, span, string_view, non null pointers, RAII, type safe generics, strong typed enumerations, safer casts,...

              Among many other improvements, that WG14 will never add to C.

              • pron3 days ago |parent

                Again, when comparing two languages you can't just look at the advantages one of them has over the other. There's no doubt C++ has many important advantages over C. The reason to prefer C in certain situations is because of C++'s disadvantages, which are as real as its advantages. Even one of the things you listed as an advantage - RAII - is also a disadvantage (RAII in C++ is a tradeoff, not an unalloyed good). A comparison that only looks at the upsides, however real, gives a very partial picture.

                Alongside all of its useful features, C++ brings a lot of implicitness - in overloading (especially of operators), implicit conversion operators, destructors, virtual dispatch - that can be problematic in low-level code (and especially in restricted environments). Yes, you can have an approved subset of C++, and many teams do that (my own included), but that also isn't free of pitfalls.

                • pjmlp3 days ago |parent

                  There isn't anyone pointing a gun to someone's head forcing them to using 100% of all C++ features in every single project.

                  There is an endless list of edemic C pitfalls that WG14 has proven not to care to fix.

                  Auto industy has come up with MISRA, initially for C, exactly because of those issues.

                  Ideally both languages would be replaced by something better, until it doesn't happen, I stand by my point, the only reason to use C instead of C++, it not having a C++ compiler available, or being prevented to use one, like in most UNIX kernels.

                  I hold this point of view since 1993, having used C instead of C++ was only when obliged to deliver my work in C, due to delivery requirements where my opinion wasn't worth anything to the decision makers.

                  So if I was already using C++ within the constraints of a 386 SX, running at 20 Mhz limited to 640 KB ( up to 1 MB) RAM size, under MS-DOS, I certainly will not change it for the 2025 computing world reality.

                  • 17186274402 days ago |parent

                    > There isn't anyone pointing a gun to someone's head forcing them to using 100% of all C++ features in every single project.

                    Tell me how to use C++, without using RAII. You can't. Not being able to automatically allocate without also invoking the constructor is what I dislike the most in C++. Other things are, that you can never be sure, what a function call really does, because dynamic dispatch or output parameters aren't required to be explicit.

                    > I hold this point of view since 1993, having used C instead of C++ was only when obliged to deliver my work in C, due to delivery requirements where my opinion wasn't worth anything to the decision makers.

                    https://floooh.github.io/2019/09/27/modern-c-for-cpp-peeps.h...

                    C isn't ANSI C anymore!

                  • pron3 days ago |parent

                    I, too, wrote C++ for the 386 in the early nineties, and I, too, generally prefer it to C, but the fact remains that it has some real disadvantages compared to C. From the very early days people talked about exercising discipline and selecting a C++ subset - and it can and does work - but even that discipline isn't free. Avoiding destructors, for example, isn't easy or natural in C++; explicit virtual dispatch with hand-rolled v-tables is very unnatural.

      • lelanthran4 days ago |parent

        > C folks rather reproduce badly C++ than acknowledge its Typescript like improvements over C

        This is a rather crude misrepresentation; most C programmers who need a higher level of abstraction than C reach for Java, C# or Go over C++.

        IOW, acknowledging that C++ has improvements over C still does not make the extra C++ footguns worth switching over.

        When you gloss over the additional footguns, it looks like you're taking it personally when C programmers don't want to deal with those additional footguns.

        After all, we don't choose languages based on which one offers the most freedom to blow your leg off, we tend to choose languages based on which ones have the most restrictions against blowing your leg off.

        If your only criteria is "Where can I get the most features", then sure, C++ looks good. If your criteria is "Where are the fewest footguns", then C++ is at the bottom of the list.

        • pjmlp4 days ago |parent

          Nah, it is called life experience meeting those kind of persons since the 1990's, starting on BBS forums.

          My criteria is being as safe as Modula-2 and Object Pascal, as bare minimum.

          C++ offers the tools, whereas WG14 has made it clear they don't even bother, including turning down Dennis Ritchie proposal for fat pointers.

          • lelanthran4 days ago |parent

            >> looks like you're taking it personally

            > it is called life experience meeting those kind of persons

            Looks like you are confirming that you are taking it personally.

            I don't understand why, though.

            You cannot imagine a programmer that wants fewer footguns?

            • pjmlp4 days ago |parent

              Yes, when careless programmers are responsible for critical infrastructure systems, and rather take a YOLO attitude to systems programming.

              • lelanthran4 days ago |parent

                > Yes, when careless programmers are responsible for critical infrastructure systems, and rather take a YOLO attitude to systems programming.

                Well, that's a novel take: "Opting for fewer footguns is careless". :-)

                It's probably not news to you that your view is, to put it kindly, very rare.

                • pjmlp4 days ago |parent

                  Is it? Ask the governments and respective cyber security agencies.

                  And to finish this, as I won't reply any further,

                  "A consequence of this principle is that every occurrence of every subscript of every subscripted variable was on every occasion checked at run time against both the upper and the lower declared bounds of the array. Many years later we asked our customers whether they wished us to provide an option to switch off these checks in the interests of efficiency on production runs. Unanimously, they urged us not to--they already knew how frequently subscript errors occur on production runs where failure to detect them could be disastrous. I note with fear and horror that even in 1980 language designers and users have not learned this lesson. In any respectable branch of engineering, failure to observe such elementary precautions would have long been against the law."

                  -- C.A.R Hoare's "The 1980 ACM Turing Award Lecture"

                  • lelanthran3 days ago |parent

                    The problem you facing is that any argument or quote that is used to dismiss C due to safety concerns applies to C++ more than it applies to C.

                    Like this quote here that you posted.

                    • pjmlp3 days ago |parent

                      Nope, and this is really the last comment.

                      First of all the quote there is indirectly about C with UNIX's adoption starting out of Bell Labs, C++ would only become known to the world in 1989, with the release of CFront 2.0.

                      Second, while you certainly can code C in C++, just like one can code JavaScript in TypeScript, the tools are there on the type system for anyone that cares, tools that WG 14 has proven not to care in 50 years of history.

                      Third, all C compilers worth using in professional scenarios are nowadays written in C++.

                      As last point, while C++ has the heads up over C in type system improvements, it is by no means the final answer in systems programming, ideally both programming languages should be replaced by better, safer ones, which there are a few already to chose from.

                      Unfortunely as long as LLVM and GCC are around, industry standards based on C and C++, there is little hope that those improved languages fully take over.

                      Thus when it comes to C vs C++, in such world where else gets to replace them in all scenarios, C++ is the only answer to that duality of choice, unless one wants to keep re-inventing solutions (badly) for answers C++ is providing since 1989.

                      • lelanthran3 days ago |parent

                        Well, look; I replied initially because you're misrepresenting "I want fewer footguns" with "I don't care".

                        The only question was whether you're doing it on purpose or whether you really do think that there are zero programmers who want fewer footguns.

                        As far as the C vs C++ thing goes, if your measure is "How many OPTIONAL features does a language give me WRT safety", then sure, C++ is optionally safer than C.

                        If the measure is "How many extra footguns does the language provide", then no, C++ cannot, by any objective measure, be safer than C.

                        Since you're constantly re-framing the discussion towards "More features" and away from "fewer footguns", I think it is safe to say that "extra footguns", for you anyway, doesn't mean "less safe".

                        The way you constantly re-frame, though, reflects quite poorly on you - it's pretty obvious from the very first thread I am counting the footguns as the measure of how unsafe a language is. That's not an irrational measure, and yet you are willing to strawman the argument to make it seem irrational.

    • flashgordon4 days ago |parent

      Actually this was my first instinct too. Just limit what you use c++ for and write c code with templates and be done with it.

      The problems I am guessing start when you are tempted into using the rest of the features one by one. You have generics. Well next let's get inheritance in. Now a bit of operator overloading. Then dealing with all kinds of smart pointers...

      • eptcyka4 days ago |parent

        What would be the detrimental effect of using smart pointers?

        • flashgordon2 days ago |parent

          Oh i didnt mean that smart pointers were bad or detriemental. Or even that any of the other gazzilion features of C++ were bad (or detrimental) on their own. Just that C++ has sooo many features that i dont think there is any one in the world who knows who any one feature X interacts with feature Y so your ability to reason about what you have written in C++ is significantly lowered.

          If you can say I will only use features A, X an Z of C++ and some how enforce it then you are mitigating a lot of the risk. IIRC Carbon (Google's new lang to migrate their code off C++) came about because they themselves used C++ in a very bounded way (I recall a lot of the templates they created for their use of C++ actually resembled how Go code looked like and may have been one of the reasons for creating Go). But I am not sure how many mere mortals have that kind of tooling and discipline to limit themselves?

        • 17186274402 days ago |parent

          Unclear, not explicit ownership.

    • hawk_4 days ago |parent

      Is there a way to pass compiler switches to disable specific C++ features? Or other static analysis tools that break the build upon using prohibited features?

      • 17186274402 days ago |parent

        There is -fno-rtti, -fno-exceptions, -Wmultiple-inheritance, -Wvirtual-inheritance, -Wnamespaces, -Wsuggest-final-types, -Wsuggest-final-methods, -Wsuggest-override, -Wtemplates, -Woverloaded-virtual, -Weffc++, -fpermissive, -fno-operator-names and probably many more. The warnings can be turned into errors, e.g. -Werror=namespaces.

      • fweimer4 days ago |parent

        No two development groups agree on the desired features, so it would have to be a custom compiler plugin.

        You could start with a Perl script that looks at the output of “clang++ -Xclang -ast-dump” and verifies that only permitted AST nodes are present in files that are part of the project sources.

        • hawk_4 days ago |parent

          For sure no two groups want the same subset but is there no "standard way" to opt in / out in the ecosystem? It's strange that there are large orgs like Google enforcing style guidelines but manual code reviews are required to enforce it. (or may be my understanding of that's enforced is wrong)

      • pjmlp3 days ago |parent

        Yes, via static analysis tools it is possible.

        As usual with additional tooling, there must exist some willingness to adopt them.

  • variadix4 days ago

    You can create macro functions per generic function so something like Vector_New(int)(&v) expands to Vector_New_int(&v). It also looks less foreign (more like templates) than the G macro.

  • synergy204 days ago

    or use nim metaprogramming, which will be transpiled to c

  • giantpotato4 days ago

    vec_push doesnt check for realloc failure in vec_fit

  • 4 days ago
    [deleted]
  • codr74 days ago

    No. You don't mess around with the language like that.

    This is how you do it:

    https://github.com/codr7/hacktical-c/tree/main/vector

    • xigoi4 days ago |parent

      This is the type erasure approach, whose cons are mentioned in the article.

      • codr73 days ago |parent

        It's the C approach, strong typing doesn't mix well with raw memory access.

        • 17186274402 days ago |parent

          The very article we are discussing shows you a way how to do that type safely in C. It uses #include which is a very idiomatic way to do this in C, and an example where a preprocessor-based approach is more flexible, than e.g. a module system.

        • xigoi3 days ago |parent

          Nim is strongly typed and allows raw memory access just fine.

          • 2 days ago |parent
            [deleted]