1. 14
  1.  

  2. 8

    Yes, you can write it. Now try debugging it. I’ve written generic data structures in C like this and the debugging experience is awful. If something goes wrong, you’re stepping through functions that are declared inside macros and most debuggers really suck at that.

    Oh, and this isn’t actually the best way of doing it. It’s much better to use a macro for your type and function names and put each of your data structures in a separate header. You can then #define the type that you want (and any other generic parameters) and #include the header at the top of your source file. You then have all of the types available in your compilation unit and the debugger is walking into real functions when you try to do them.

    After you’ve done this, you should step back and realise that you’ve implemented a subset of C++ templates with even worse syntax. At that point you should ask yourself whether that’s really what you wanted. If you really want to write C++ code in C, why not just use C? You then get a standard library that’s full of features that make this easy. Even in a free-standing environment, things like <atomic> and <type_traits> are very useful and the C++ atomics are far better designed than the C versions (which are a half-arsed port from C++11 to C11 that completely missed the point in a few places).

    I recently wrote some code using C++ templates that would be almost impossible to make both readable and portable in C. The goal is to pull the system call number and arguments out of a signal frame, call a function that emulates the syscall, and then insert the result into the response. The code for pulling out the system call arguments is per platform and is very simple. There’s then a template function that extracts the arguments into a tuple with the correct type and a that, in turn, is called from a generic lambda that gets the types from the declaration of the function that emulates the syscall and invokes it. This is fairly trivial in a language with a built-in generic system and some pattern matching on types. Doing the equivalent in C would be horrendously painful. It would probably be easy in Rust too. Oh, and if you’re worried about binary size, this whole thing compiles down to about a dozen x86 instructions - the compiler extracts arguments in blocks for functions that use the same number of arguments and then switches on the syscall number to invoke the handler function. If I were doing the same thing in hand-written assembly, I probably wouldn’t generate better code.

    C++ is generally available on any 32-bit platform with a C compiler, these days the only excuse for choosing C is that you have a weird 16-bit target with no C compiler.

    1. 1

      If you’re targeting a compiler with pre-C++11, or worse…. pre-C++98, I could see just wanting to use C instead. Early C++ is rough.

      1. 1

        That’s true. C++11 made the language tolerable, C++17 made it nice. Clang and GCC both support C++17 though, so unless your target is something sufficiently exotic that it isn’t supported by those, you’re probably in luck.

    2. 3

      I’ve probably read something like this before, but this time it hit me and hopefully will stick because it is a helpful quote:

      Rust and Haskell choose flexibility and safety. Go chooses safety and simplicity.6 C chooses flexibility and simplicity. Each choice has its trade-offs.

      1. 3

        It sounds nice, but it’s glossing over a lot of nuance that is there. “Simplicity” applied to programming languages is at best an ambiguous term.

        simple == few features is not the same a simple == easy to use, but this equivalence is often implied. An extreme counter-example is Brainfuck which is objectively a simpler language than C in the first meaning, but absolutely not in the second meaning.

        C has very few features on the surface, but a good C programmer needs to know things that are “between the lines” in the language, like undefined behavior, thread safety, and memory management patterns. C doesn’t have a borrow checker, but a C programmer that doesn’t understand memory ownership will likely cause leaks and use-after-free.

        And whether a language is actually simple to use depends on the task you use it for. If you need to write a pervasively multi-threaded program, C is one of the hardest languages there is.

      2. 3

        “The marvel is not that the bear dances well, but that the bear dances at all.”

        Or:

        “Problem: You need a generic data structure. / I know! We’ll use the preprocessor! / Now you have two problems.”

        If one is willing to go to such lengths just to avoid switching to C++, I wonder if there’s some hidden past trauma. Perhaps a C++ programmer killed your family when you were a child and you’ve repressed the memory?

        1. 1

          I think there is a slight danger in returning an actual IntStack object – the user has to be careful never to pass it by value to a function that modifies it.