Hacker News new | past | comments | ask | show | jobs | submit login
Common libraries and data structures for C (github.com/tezc)
322 points by tirrex on May 16, 2022 | hide | past | favorite | 141 comments



Okay, I love C so of course I had to take a look. The buzzwords are impressive, with all the testing and CI and so on, really nice, modern and ambitious.

I dove into the code, literally looking at the first true part of the implementation in array/sc_array.h.

Two observations, from probably less than tree minutes of reading:

1. The nomenclature with "sc_array_term()" as the destructor (the opposite of sc_array_init()) was new to me; I'm not saying that's a real problem but it's at least adding friction in a way. I would have expected sc_array_free(), but that seems to be internally used as an alias for the standard function free(). Very confusing, since that name (sc_array_free()) then leaks into the source but it has the feeling of being meant for internal use.

2. I wonder if this has a bug:

    /**
     * Deletes items from the array without deallocating underlying memory
     * @param a array
     */
    #define sc_array_clear(a) \
    do {                                                                   \
      (a)->cap = 0;                                                  \
      (a)->size = 0;                                                 \
      (a)->oom = false;                                              \
    } while (0)
In my experience, when you want to clear a dynamic array but keep the allocated memory, the 'capacity' ('cap', here) field should not be cleared. I think this will leak memory if an array is grown, cleared, and then re-filled since the sc_array_add() function will see a zero capacity, and allocate new storage.

Just my inflation-devalued SEK 0.02, and I did not run the code, I just read it very quickly. Corrections welcome.


> 1. The nomenclature with "sc_array_term()" as the destructor (the opposite of sc_array_init()) was new to me; I'm not saying that's a real problem but it's at least adding friction in a way.

I think there shouldn't be any need to innovate in the naming of these kinds of functions... GLib got it perfectly right [0] with the pair init / finalize. One might like the alternative init / deinit if the goal is to have some symmetry in the naming... but there are not many better choices.

free is to be used (always IMHO) typically as the opposite of new, make, or create.

"Naming things is difficult" and all that...

[0]: https://docs.gtk.org/gobject/concepts.html


>... /* * Deletes items from the array without deallocating underlying memory * @param a array */

This does look like a bug, if not functionally, then by the described intent. The comment states that the intent is to delete items without deallocating; yet zeroing the capacity cuts any future way to deallocate properly, unless it's just an array of simple types (free(baseptr)). If there are any pointer types, then there will remain no safe way to deallocate the elements' contents.

Sure, there could be other ways of keeping inventory of the allocated memory, but this detaches the array from knowing its allocated bounds.


The macro does not look (necessarily) as a bug, it simply does a counter intuitive thing of zeroing everything, including the capacity. Maybe the way it is used, the capacity is saved, then the array cleared, then it is set back; or more likely it is used only after the allocation of the object, when everything requires to be zeroed (but if this is the case it should be called "_init" and not "_clear", for clarity). Does not look as the most sounding interface of course. Also this is the kind of thing that should not be done as macro regardless of speed... and only turned into a macro in case of very aggressive profiler-drive optimization.


If the way it is used requires the user to break the abstraction/encapsulation and manually buffer some fields in order not to break the data structure and leak memory, I would call that a bug.

There is one use of sc_array_clear() in the test code [1] which really makes it look as if it is being used in a way that I think (again, I haven't single-stepped this code, only read it) leaks memory.

I agree on the pain of everything being macros, it's more pain than it's worth I think and will likely lead to code duplication (and more pain in debugging, probably).

I would even go so far as to think that this kind of single-file design, where each file is independent of the others, makes it harder and more annoying to implement more complicated data structures.

[1]: https://github.com/tezc/sc/blob/master/array/array_test.c#L3...


I'm too lazy to look at the code that does the alloc, but if this came my way in a PR I'd ask if the code doing the allocation is using malloc or realloc.

If all allocations are performed using realloc[1] then I have no problem with that macro.

[1] Sometimes it's just easier. Why conditionally call malloc/realloc when you can simply call realloc all the time? Realloc(NULL, size) is equivalent to malloc(size).


> it simply does a counter intuitive thing of zeroing everything

It might not be an implementation bug, but that is just asking for bugs. Possibly very hard to find bugs.


Glanced over it, it doesn't look like a bug, just wasteful.

The way I see it, because sc_array_clear resets cap and size to zero, the next call to sc_array_add will issue a realloc call to the default minimum size (8) and it won't leak because sc_array_clear doesn't overwrite elems.

So yeah, not cool but doesn't look like a bug either.


I see just one GH issue and it is none of yours above, why?

As for someone claiming so much appreciation for C it seem like your love is a bit toxic ;-) (no offense here).


As I got more into C programming, I started looking for data structure libraries. Found a few [0]. Also evaluated sc, but it had too much pre-processor magic for my taste. It also bundles random "stuff" like a URI parser, a thread abstraction, etc.

Eventually I rolled my own [1] more focused library. It's basic and portable.

0: https://begriffs.com/posts/2020-08-31-portable-stable-softwa... 1: https://github.com/begriffs/libderp


I think it's common for C programmers to roll their own. I did the same [0].

I went pretty deep into composable C templates to build mine so it's more powerful than most. The containers can handle non-bitwise-movable types with full C++-style lifecycle functions and such, and the sort algorithms can handle dynamic and non-contiguous arrays (they are powerful enough to implement qsort() [1], which is more than I can say for any other C sort templates I've seen.) My reasoning for the complexity at the time was that any powerful container library is going to be reasonably complex in implementation (as anyone who's looked at STL source code knows), so it just needs to be encapsulated behind a good interface.

I'm not so sure that's true anymore. These sorts of simpler libraries like the one linked here definitely seem to be more popular among C programmers. I think if people are using C, it's not just the C++ language complexity they want to get away from, but also the implementation complexity of libraries and such. There's a balance to be had for sure, and I think the balance varies from person to person, which is why no library has emerged as the de facto standard for containers in C.

[0]: https://github.com/ludocode/pottery

[1]: https://github.com/ludocode/pottery/tree/develop/util/potter...


Thanks for sharing! This looks like a very well designed container library.


I may as well throw my hat into the ring: https://github.com/lelanthran/libds

I decided that I wanted to be able to simply drop a single .h file and a single .c file into any project without have to build a `libBlah.so` and link it to every project that needed (for example) a hashmap.

The practical result is that using the hashmap only requires me to copy the header and source files into the calling project.

It does build as a standalone library too, so you can link it if you want.

My primary reason for starting this is that I was pretty unsatisfied with all of the string libraries for C. When all I want to do is concatenate multiple strings together, I don't want to have to convert between `char *` and `struct stringtype *` everywhere.

The string functions are very useful as they all operate on the standard `char *` (nul-terminated) type.


Did you look at glib? It has quite a lot of features and is pretty easy to use.


Good stuff. I may make use of the .ini parser as my current one doesn't handle sections.

There are a growing number of stand alone support modules in my projects that I need to publish as a collection some day. Here's a couple links to some of them:

- https://github.com/WickedSmoke/faun/tree/master/support

- https://github.com/xu4-engine/u4/tree/master/src/support

These include more exotic stuff like a grid based field-of-view calculation and a version of the sfxr synthesizer.


For people advocating use of C++ instead of C, keep in mind there are several platforms (mostly embedded) that only support C and not C++. Also there are many projects that make use of C only. If C++ is available, I agree one should use it, however that is not always the choice.


> If C++ is available, I agree one should use it,

C is very simple, while C++ is a monster with 100 heads, each speaking in another language.

If all members of a project use the same subset of C++, then yes, it would be preferable over C. But if anyone or any team in a large project uses whatever he wants, things can get pretty complicated, pretty fast.


In C, whenever any part of the creature needs to do some thinking, it grows its own small head to do just enough thinking to get by, and then goes about its business. The process is simple, but the result is often Cthulhic.


If the team doesn't care about code reviews, static analysis and automatic formating tools on PR merges, surely.


Unless we are talking about something like Z80, 6502, PIC, AVR and similar low end CPUs, which hardly full support even C89, all modern CPUs have C++ compilers available.

More often than not, it is a matter of not wanting to use C++ than it not being available.


On most embedded systems you cannot include std. You don't have a decent memory allocator. Bad use of templates will consume all of your flash space (code duplication).

Yes, it can work for simple Arduino sketches, and I won't say that there are not complex embedded projects in C++.

But... why should I want to use C++?

For one, I use a small subset, like it's C with classes. I like function overloading and default argument values, initializing default values for some structs, not having to type typedef struct, the fact that for a time I could declare variables mid-function and using literal bool, true, false, etc.

But full-blown C++ on embedded (MCU)? I think I'll pass.


You cannot also use stdio and most stdlib, so what?

You would want to use C++ for stronger type safety, proper enumerations, templates instead of undebuggable macros, namespaces,....

https://news.ycombinator.com/item?id=31406942

"CppCon 2016: Jason Turner “Rich Code for Tiny Computers: A Simple Commodore 64 Game in C++17”"

https://www.youtube.com/watch?v=zBkNBP00wJE

MCUs also don't support full-blown C, and it isn't a show stopper to keep advocating it, so why should it be for a language with better safety options, even with constraints.


> You cannot also use stdio and most stdlib, so what?

You can still use a subset of stdio and stdlib. Newlib also provides hooks you can use to implement fopen and the like, if you want.

> better safety options

Safety is not always the utmost priority. At least, not that kind of safety. It seems people forget this concept from time to time.

> templates instead of undebuggable macros

This is if you use complex macros, which is not always the case. I've been doing embedded for 20 years and I rarely (very rarely) find problems with macros.

But you know what's undebuggable on embedded? Complex inheritance, which sooner or later you'll hit with OOP.

For a system in which you don't have a screen, or logs, or any kind of output, that is usually not in your table, and you have to study failures by telling someone to look at an LED, or by guessing what could have gone wrong, you have to keep your system simple.

Add templates to the mix and good luck opening the project six months later, when a customer calls with a problem.

Edit: But again, used with caution, C++ can be used in embedded, at least the way I use it, as explained before.


The same subsets are available in C++ as well, with stronger type checking and RAII for closing those handles.

Yeah, safety and IoT unfortunately aren't something that go together.

Using C++ doesn't require OOP all over the place, nor crazy template metaprogramming.

The same reason you give for using macros.


> RAII

C++ is used mostly with exceptions disabled on embedded, so failing constructors are a PITA. You have to keep track of valid states with a bool. Now every function entry has:

    if (!m_bValid)
        return;
I hate it.

> Yeah, safety and IoT unfortunately aren't something that go together.

Don't forget that IoT is just a minuscule fraction of embedded. Not everything is connected online or requires safety as top priority.

> Using C++ doesn't require OOP all over the place, nor crazy template metaprogramming.

So we agree. But I think you should be considering the possibility that there might be something going on between you and C, and that it could be possible that not every C developer out there is out of their mind (considering the quantity of past, present and future C projects).

Make peace with C, because the world runs on C and we'll be long gone by the time C will be replaced by something else :)


Yeah, checking return codes is a thing you NEVER have to do in C...

The "exceptions are evil, but without exceptions you can't fail a constructor!" is a really irritating canard when it comes to C++. First off all, the terribleness of exceptions is way overhyped, but even if you do want to avoid them, this is not a problem: just use a factory function instead of a constructor and return a `std::optional` (or a default-constructed value and an error code if you want to avoid `std::optional` for some weird reason).

I like pure C a lot and it definitely has a place in areas like embedded. But this kind reflexive disdain for C++ using uninformed arguments is really tiresome. C++ is fine.


> the terribleness of exceptions is way overhyped

It's not that exceptions are terrible. I have nothing against them. The thing is that most of the time they are not affordable, especially in embedded. Some compilers don't even support them (some 8-bit IIRC). Most (including mine) embedded C++ programs have exceptions disabled.

> return a `std::optional`

The same goes for std. I don't know the overhead of possibly duplicating these classes(1) for every instance of std::optional. Most (including mine) embedded C++ programs don't use std.

30KB of extra code is nothing for desktop/server applications, but it's not convenient for a MCU with 64KB of flash.

(1): https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-...

> just use a factory function instead

This is practical with an efficient heap allocator (which I might not have). What happens if I want a scoped (on stack) instance of a class?

See how things are quickly getting more complicated with C++?


It's that amount of lines UNCOMPILED. Of course uncompiled code wouldn't fit in 64kb of flash (the compiler wouldn't fit either, you know). Compiled, std::optional takes up essentially no overhead, basically the same as if you're returning a single bool along with your object.

> This is practical with an efficient heap allocator (which I might not have). What happens if I want a scoped (on stack) instance of a class?

There is absolutely no reason to do a heap allocation for a factory function in modern C++ (or even relatively ancient C++, in fact). The fact that you would think so indicates you simply don't know C++ at all.

Tell me, where in these six lines do you see a heap allocation? Where in the total of 6 instructions this compiles down to do you see anything that couldn't run in any embedded environment? Tell me how these six instructions wouldn't fit inside your 64kb of flash:

https://godbolt.org/z/rErWPbbbx

And again, EVEN IF you're so religiously and irrationally opposed to using std::optional, you can just return a default-constructed object and an extra bool indicating successful construction. I don't know why you would considering you could just return an optional, but whatever, you can do it that way if that's what you prefer.

You're just wrong about this stuff, and it's this kind of lazy, uniformed criticism of C++ that really rubs me the wrong way. If you wanna use C, use C! Nobody's stopping you, it's a fine language. Just leave the C++ discussions to the people who actually know what the language is.


Just chill, OskarS. I know the difference between compiled code and source code. Regarding std::optional, I even admitted "I don't know the overhead of possibly duplicating...".

30KB was a ballpark about exceptions plus std in general, by including things you expect to use when using std (like std::string).

The patterns I knew and saw with std::optional usually returned "new something", but yeah, you can return a copy too if the class fits in the stack.

And I am not religiously and irrationally opposed to anything.

> really rubs me the wrong way

Relax, my friend. Life is short.


> The patterns I knew and saw with std::optional usually returned "new something", but yeah, you can return a copy too if the class fits in the stack.

Oh, fer crying out loud... It DOES NOT return a copy, it constructs it directly in place. This is called "copy elision" (also known as "return value optimization", or RVO), and has been done by compilers for three decades, and is actually mandated behaviour in modern C++. I don't know where you saw these examples, but you would never use operator new with std::optional: it takes a value, not a pointer.

You have a very annoying style of being wrong about absolutely everything you say, and then acting superior about it when people call you on your bullshit.


In the history of C++, RAII predates exceptions by several years.

When I got introduced to C++, via Turbo C++ 1.0 for MS-DOS in 1993, I decided that there was hardly a reason to use C other than being forced to use it by external reasons.

Thankfully there are plenty of places that have moved beyond running on C. :)


> beyond running on C

Can we agree that for at least the past 25 years, 99% of devices runs on C and/or depends on C and/or relies on C, or its development ran/relied/depended on C? From Windows/MacOS/Linux kernels up to the FW in your mouse, keyboard, monitor and HDD controllers.

It's difficult to escape C :)


Using C compilers written in C++, yeah really difficult to escape C.

Windows has been migrating into C++ since Windows Vista, when VC++ introduced kernel support. Its C universal runtime is written in C++ with extern "C" entry points. Ah and then there is COM/WinRT, also mostly about C++ and .NET, even if the ABI is compatible with C (for masochists).

macOS uses C++ for drivers, a kernel without drivers isn't of much use.

Speaking of which, in Android all drivers written using Project Treble (required as of Android 8) are either C++ or Java based, and now Rust is also into the picture. The only C is the "classical" Linux upstream stuff.


> More often than not, it is a matter of not wanting to use C++ than it not being available.

That's perfectly valid. I reach for C when I need to write fairly small programs. When C does not fill my needs anymore, I don't reach for C++. There are better high-level languages.

C++ is not a replacement for C. When one of the worlds foremost experts on C++ complains that the language is too complex to understand[1], passing on C++ is a reasonable position.

[1] https://steven.brokaw.org/posts/scott-meyers-cant-remember-c...


Same critic can be applied to any language of the same age as C++.

Try to do a pub quizz about a begginiers friendly language like Ruby or Python, that spans over all their versions.


I agree with you. I'm a bit tired of C++ being imposed on those that rather just use C or as if C users can't turn to any other options besides C++. To include that C interop has become a thing for many newer programming languages now.

By the way, the D language (which has been with us for a while) has long positioned itself as a viable alternative to C++.


Why do you mention AVR? AVR-GCC has C++ support, that's what made the original Arduino (before they switched to ARM) approachable to beginners.


> AVR-GCC has C++ support

It's a very restricted subset of C++ that has almost no advantages over plain C, and some number of disadvantages.


- templates instead of macros

- constexpr instead of macros

- if constexpr instead of macros

- stronger type checking

- type inference

- strong type enumerations

- namespacing instead of pre-historic naming prefixes

- classes as means to enforce type invariants

I see some advantages.


List of typical 8 bit CPUs, and I was thinking only about commercial compilers.


When working on some platforms (e.g. BeagleBoneBlack), the compilation time of C++ is a huge turnoff. Sure, you can cross-compile, but that feels like even more friction.


Wait you ssh into a BeagleBoneBlack and compile C on the device?

I’ve seen some janky envs in my life but this is pretty high up. Do you never enable LTO when linking deps? These devices are literally 1000x slower than a typical 8 core desktop.


Yes, and edit/debug code in the device too. Setting up an identical cross compilation environment with all the same shared libraries (it's a full Linux system after all) and such is non-trivial and C compiles fast enough it's fine... (1000 times is a bit of an exaggeration anyway...)

Now maybe if I were an emacs user this setup wouldn't work...


Compiling a single C file program, linking glib, libarchive and some other stuff (repeated runs, to allow for caching inputs):

Orange Pi 3:

    real 0m0.916s
    user 0m0.839s
    sys 0m0.074s
5950X

    real 0m0.080s
    user 0m0.064s
    sys 0m0.016s
Slower, but not nearly 1000x in practice. Cortex-A8 is a bit slower, still, but not by much.


well, one project I work on compiles ~100 times faster on my desktop (Ryzen 5 3600) than the BBB if building from scratch, and that's not accounting for multiple cores, so 1000x might be reasonable for a project with sufficient compile-time parallelization. But it's still fast enough (< 10 seconds ) on the BBB that setting up a cross-compilation environment isn't worth it (and obviously incremental compiles are faster).


> Wait you ssh into a BeagleBoneBlack and compile C on the device?

Exactly how am I supposed to access the hardware that only exists on the Beaglebone Black itself, otherwise?

You can cross-compile all day, but, in the end, I need to code to be running over there to toggle those GPIOs. I need the debugging to be over there. I need VSCode to be running over there.

Not all programming is web programming.


I mean, theoretically it's possible to set up a cross-compilation environment (which would require mirroring basically the entire /usr of the BBB) and copy over compiled files for running / debugging. But it's only worth it if compiling takes a long time (or I suppose if you were to use templaty C++ or rust or something, you may not have enough memory on the BBB to actually compile things... but maybe don't do that?).


rust actually does okay on the BBB unless you activate heavy macros at which point the memory blows up, and it grinds to a halt.

rust-analyzer, for example, could be built (slowly) on the BBB until recently. The last refactoring of it ... ooof ... did something to macros and now the memory footprint is gargantuan.


Somehow I enjoy writing C much more than C++


Embedded devices really opened my eyes to this world. What a fool I was using malloc on my first Arduino project.


Arduino is entirely C++ though


> Arduino is entirely C++ though

No, it's C++ without exceptions (so constructors are useless).

On Arduino you will almost certainly program in a C-with-classes manner; lack of exceptions make almost everything that C++ brings to the table useless[1].


> lack of exceptions make almost everything that C++ brings to the table useless[1].

sorry what ? constexpr (https://github.com/arduino-libraries/Arduino_EdgeControl/blo...), templates (https://github.com/arduino-libraries/Temboo/blob/dcce3007eb5...), etc. are all widely used in the Arduino community. I've recently done some work involving C++20 coroutines on an RP2040. etc etc...

And even without exceptions, RAII can still help ensuring a large amount of invariants automatically.


It is remarkable how many large and modern C++ code bases somehow make thorough use of the language, including constructors, without exceptions. Unless the asserted need for exceptions is a gross mischaracterization.


There's the Clib initiative at https://github.com/clibs. I don't know how much they curate/review their entries.

As often said, apt install foo is also a bit of a package manager for C.

Maybe we should establish a sort of expert-led central archive of rock-solid, battle-tested C libs/functions/snippets that one can trust ?


    Maybe we should establish a sort of expert-led central archive of rock-solid, battle-tested C libs/functions/snippets that one can trust ?
GNUlib [1], albeit marketed as a "portability library", in fact shares a lot of that goal and includes data structure implementation, OS interfaces, etc. A couple of excerpts from the docs:

* "Gnulib is intended to be the canonical source for most of the important “portability” and/or common files for GNU projects. These are files intended to be shared at the source level" [3]

* "We develop and maintain a testsuite for Gnulib. The goal is to have a 100% firm interface so that maintainers can feel free to update to the code in git at any time and know that their application will not break." [3]

[1]: https://www.gnu.org/software/gnulib/manual/html_node/index.h...

[2]: https://www.gnu.org/software/gnulib/manual/html_node/Gnulib-...

[3]: https://www.gnu.org/software/gnulib/manual/html_node/High-Qu...


Well.. that'll do it ! I wonder how I never stumbled upon that massive trove.


Interesting project. We have a similar set of various cross-platform helpers for various C structures and tasks during all these years within Rizin - RzUtil[1][2]. They are more tightly coupled with each other though and is LGPLv3-licensed.

[1] https://github.com/rizinorg/rizin/tree/dev/librz/util

[2] https://github.com/rizinorg/rizin/tree/dev/librz/include/rz_...


I work in C++ daily and there is something about the simple-ness of C that I love. You get out of magical hell that is templates and return to simple flat-functions and macros.


Templates suck because they're the wrong abstraction. Or rather, they pretend to be an abstraction when in reality they're leaky as hell.

Rust's traits are generics done right. I haven't looked at Concepts too much, I hope that they'll be better than templates.


They are, the best ISO C++ alternative is ISO C++ vnext.


D is more appealing and powerful, and if you are familiar with C/C++ there's not much of a learning curve - https://dlang.org/overview.html


Hot take: people make a bigger deal of how complex templates are. Modern compilers find and describe errors much better than in the days of trying to use boost in vc6.


Nothing prevents you to code like that in C++, although I would rather suggest templates instead of macro hell.


That's the thing with C and C++.

C is light and simple, but don't use it much, because it can get too verbose.

C++ allows for succinct code, but it's neither light or simple. Nor does it have any concern for the elegance of its design.

Hence the common practice of using a small subset C++ and pretending it's just C with Extras.


> Nor does it have any concern for the elegance of its design.

It has a lot of inelegant facilities, which, when used under the hood, allow you to express your elegant abstractions.

> Hence the common practice of using a small subset C++ and pretending it's just C with Extras.

That's mostly failure to use C++. Since C++11, and especially with later updates to the standard, idiomatic C++ is very different from C - even if you're not using a lot of the standard library. I'll link to a talk already linked-to in another comment in this discussion:

Rich code for tiny computers / Jason Turner, CppCon 2016 https://www.youtube.com/watch?v=zBkNBP00wJE


> That's mostly failure to use C++.

Isn't this the no true Scotsman fallacy? It looks like you're agreeing with the parent poster that a lot of people use a small subset of C++ to pretend that it's C with extras. If this is true, it's not a failure of all these people because they don't _really_ understand C++. It's a failure of the language designers because they have made something that nobody can agree on how to use.


I should have used a different word. I meant "failure to do X" as in "not doing X", not as in "lack of success in the attempt to do X"

See the two meanings here:

https://dictionary.cambridge.org/dictionary/english/failure


> That's mostly failure to use C++.

How is it a failure when people just find idiomatic C++ undesirable, exactly because it's very different from C.

Basically, the best C++ feature by a very long mile is that it can in fact be used as an extension of C. That's what made it popular in the first place and that's still "what people want".

If it's a failure, then it's that of the C++ committee evolving the language in an echo chamber.


You can take a look at C# generics. They are quite simple.


Good stuff. void* in itself is great for generics. C does support a templating system, like C++, with a little (reasonable) preprocessor abuse. It gives great cache contiguous results: https://www.github.com/glouw/ctl


Probably best to disclose you're pitching your own baby to us.

What do you feel this content adds in context of the article? I can't tell.


You mean why would anyone link another useful C library in a thread about C libraries - you can't tell?


Without any explanation it's probably not useful to most of us.


I thought "glouw" in the username here and in the username in the link was pretty obvious myself.


I mean, it’s not like I’m plugging retail source ;)


Yeah like I’m not gonna complain to my ad blocker :-)


they're kind in the same category to me, good to know both and compare them.


I'm confused. It's easy to emulate a set using a map, but how do you emulate a map using a set?


You model the key->value relationship, call it an association, and place associations in the set, where the equality/hash functions are performed on the association key.


With apologies to Greenspun:

Any sufficiently complicated C program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of C++.


The Linux kernel doesn't.


The lengths people will go to not use C++ is staggering.


> The lengths people will go to not use C++ is staggering.

Every C thread on every forum always has some C++ proponent jumping in to bemoan that people prefer one of the simplest languages over, literally, the most complex (from a programmer PoV) language in existence.

Is it really such a stretch to believe that people prefer readability and maintainability over expressive power?

The "lengths" in this case is small - a once of cost of a few days to implement vector and string functions gets you most of what most developers need and want.


Yeah, having to define all vector functions as macros is definitely the epitome of readability and maintainability.


In this specific case I think we have to agree with your point. When code have to go all-.h, it’s already reasonable to go [a subset of] C++.


Every single time there are some people who love C (in my experience for the reason they haven't worked professionally in C++) that claim that they're actually WAY more productive and readable when working in C.

C++ is basically almost a perfect super-set of C. Use the parts of C++ that are useful to you, don't use the parts that aren't. Clearly std::vector, std::string, std::unordered_map would be ultra useful to anyone. Especially because they are easier to use, more optimized, not as prone to errors as their C equivalents.

I blame Jonathan Blow and Muratori for this. They somehow got the entire gamedev community to worship the idea of writing things in C. People grow out of it really fast though, as soon as they actually learn C++.


Yes, writing libraries and tools. The horror.


I see nothing wrong in having a preference if both C and C++ can be used for the same project and if the development speed is roughly the same.


Why are people driving Nissan Leaf to work when they could be driving Veyron Bugatti? Its clearly more powerful and way faster.


I guess CCAN died?

https://ccodearchive.net/


Rusty must be too busy working at Blockstream!


What I find frustrating when I use C instead of C# is that I have to hunt for libraries and include them in the project or write my own implementation, even for most popular things like data structures, search algorithms, sorting algorithms, serialization, http calls. Whereas in C# the framework will provide them for me. If something is not in the standard library, I can use a directive or just reference a method from a package and the IDE will help to install the package and reference it in the project.

Go and Rust are similar in that aspect.


...the IDE will help to install the package

I wish I could love that feature, but I hate it. I don't like the way it obscures what's going on under the hood (I find myself taking snapshots and trying to figure out "what's changed" or reverse engineer the installer metadata), and many projects use it as a crutch instead of providing a simple drop-in file (or a few). So many projects don't even include a simple "download" button anymore.


>Go and Rust are similar in that aspect.

I think this poses a subtle security risk about namespacing. Who authorizes these packages? Who audits these repositories?

When you use a C/C++ library, there is obvious accountability. You know who maintains the repository (usually your distribution) or you explicitly copy someone elses code as a subrepository.


If you trust the compiler, why wouldn't you trust the standard library? They're usually made by the same people.


> If you trust the compiler, why wouldn't you trust the standard library? They're usually made by the same people.

That sounds like a different way of saying "they're sometimes made by different people", which is why you won't trust it.


By different people that have been approved by the compiler developers.


I am talking about non-standard libraries


I really like the count prefixed strings, and will be adding it to my grow set of C stuff I'm gathering. My ultimate goal is to have something that does reference counted, count prefixed, null terminated strings, like Free Pascal.

I think using the cleanup attribute, and a defer macro can make it happen.


I don't understand why so many functions are implemented as macros when C99 has the inline keyword.


Interesting to look through.

I quickly scanned for things that stood out for me. As with anything, there are some things I disagree with.

To wit: the condition variable code includes a mutex inside of it, to deal with the case where you perform a signal on something before there is a waiter. Does this solve some problems? Sure. But it introduces extra overhead when I know what I'm doing and just want a condition variable.

These types of small things - the author thinks this is the "right thing", whereas others won't - are why no C standard C library like this exists. In C, the beauty is that you make every single decision; someone else's decisions won't be the same as yours.


Condition variables must always be used with mutexes.


I don’t understand how after all these years there isn’t a common library that everyone uses for this stuff. There seems to be a bunch of different ones, but not a single standout that most people use. Why not? Where is Boost for C?


The closest I’ve seen is GLib: https://wiki.gnome.org/Projects/GLib


Is there something like this but free of allocations at runtime? Things like a queue for example with a preset/defined maximum size of entries that won't suddenly grow but rather tell you that it's full. I made an allocation free JSON parser/serializer once but I don't want to reinvent the wheel all the time ... Embedded devices with low RAM are still a thing and allocation free code is often mandatory.


My old kazlib has the basics: hashing, red-black-tree, lists, exception handling.

https://www.kylheku.com/~kaz/kazlib.html

Used in e2fsck, Ethereal and others.

There is even C++ support hidden in there: the dict.h contains a C++ template wrapper (which keeps the container intrusive).


Seems to be mostly aimed at desktop OSs from a look at the source. How about one that’s platform independent, for embedded systems for example?


Looks good


I'm not a C developer, nor have I ever been interested in developing with it.

From my perspective, it seems like a massive time drain and non-productive use of my time. Just a few points:

- Tooling seems all over the place (build system, package management)

- Having to roll your own trivial functions / types (tooling may play into this)

- Versioning is confusing (C99, C11, ???)

The only advantage I see would be in embedded software because C is supposedly supported on a lot of platforms and is performant. But, I'm not actually sure this is true in practice.

Can you write one file of C code and compile it easily for multiple platforms or is there a lot of caveats?


What do you mean by 'time drain'? Do you know how much time it would take you to port your assembly code from x86 to ARM? And then when a new CPU comes out, you've got to rewrite all that assembly code again. Now that's a time drain. You could write your code once in C, and compile it for any CPU at this point. That's massive amount of your time saved.

Again, what do you mean by 'time drain'? Do you know how many human years were saved because 'grep' doesn't have to wait for the Java Hotspot VM to start up? If you spend an extra week to write a program that runs 10 seconds faster, you only need 14400 users to break even. If you've got a million users, you've singlehandedly saved humanity 115 _days_. That's a massive amount of time saved.

You must mean that you've got to code up some idea in whatever way possible so that it sees the light of day asap, regardless of how slow it is, how much energy it uses and how much disk space it consumes, because you need to sell something to someone. That's not what C is for.

C exists to write fast programs, not to write programs fast.


What I found is C compiles fast and produces small binaries.

Adding phat pointers, arrays, tagged unions, and closures to C would fix a lot of the current pain points without turning it into the disaster that is C++.


>C exists to write fast programs, not to write programs fast.

What if you want to write fast programs fast?

Also, while not useful for command line tools or for small run once programs, if the software is running continuously or its size is past a certain threshold, it might pay off to use .NET or Java since the speed is not that far of from C.

https://benchmarksgame-team.pages.debian.net/benchmarksgame/...

https://benchmarksgame-team.pages.debian.net/benchmarksgame/...


Most programs are not benchmarks.


Do you mean to support what they wrote, or do you mean to refute what they wrote?


Been doing it for 15 years. C is a very stable target and easy to use. Portability doesn't come out of the box but is doable with a single abstraction library of your preference.


C is of course a terrible waste of time for most tasks today. It is performant and widely supported though. The main thing with it is that you simply cannot avoid it in certain areas, it's C or bust.

Now, let's talk about how much time C++ wastes ;)


> Can you write one file of C code and compile it easily for multiple platforms or is there a lot of caveats?

Target POSIX, and there's very few caveats. You're looking at portability as "how many processors can this run on". Think of it more as "how many places can use my library if I wrote it in C".

You're missing the major upside of writing in C: if you write your library in C, it is callable (with no changes) from Python, Ruby, Java, C++, C#, Lisp ... just about any language, really.

The language is so simple that an automated tool can be used to automatically generate bindings from your language of choice to C. I use Swig. Look into Swig.

You see C as an ancient dinosaur with no use, the reality is that it is usually the only real way to write reusable software[1].

[1] Sure, you can write in C++, but you'd still have to wrap it in C.


C is supposely a language which it is reasonable to write a compiler for and in order to get a reasonable hardware ISA abstraction.

Don't worry, the ISO working groups are making sure that it won't last and soon writting a C compiler will become a nightmare like what they did for c++ (C23 is seriously scary).

Instead they should fix it: remove _Generic, typeof, etc which have nothing to do there, and make sure writting a C compiler does keep requiring only a reasonable amount of developement efforts (not an army of devs for 20 years like c++).

Actually, removing and hardening stuff would be more appropriate, namely not really adding stuff: remove typedef? harden and improve the type casting rules, let's be bold and eat the bullet, remove the implicit cast except to/from void* pointers and add static/dynamic casts (please not with the c++ syntax)? Make function pointers the same than the other pointers, finally (POSIX/GNU requires it though). Etc.


> Instead they should fix it: remove _Generic, typeof, etc which have nothing to do there, and make sure writting a C compiler does keep requiring only a reasonable amount of developement efforts (not an army of devs for 20 years like c++).

Have you ever written a C compiler? These things are not difficult to support. You sound more interested in just removing things that are "new" that you don't really like than genuinely concerned about things that are complex to implement, like passing structs around while keeping the platform ABI in mind, and handling the intricacies of bitfields.


I agree C23 is scary (1000+ new functions???), but your complaint seems a bit misplaced. For example `typeof` is a GNU extension, and removing `typedef` will instantly break virtually everything (which is not only used to remove `struct`/`union`/`enum` from the name). And a reasonable C compiler can only do a very limited amount of optimization, which precludes a majority of current C uses.


It looks like typeof() is being added it C23, so his complaint about it sort of makes sense.

I say "sort of" because if he's truly concerned about the effort involved in creating independent implementations, then obviously he should be evaluating features by the difficulty involved in implementing them. typedef and typeof are absolutely trivial to implement [0] [1]. typedef just creates type aliases and typeof is very similar to sizeof.

Also, I'm not sure what 1000 functions you're referring to, but most functions being added are already in POSIX and already exist in a huge number of independent libc implementations. There are dozens of POSIX libcs, including at least 5 totally independent implementations for Linux alone that are still fully maintained and under active development.

[0]: typeof: https://github.com/rui314/chibicc/commit/7d80a5136d1b2926dd0...

[1]: typedef: https://github.com/rui314/chibicc/commit/a6b82da1ae9eefa44da...


> It looks like typeof() is being added it C23, so his complaint about it sort of makes sense.

To my knowledge typeof was discussed but not adopted to C23, but well, yeah, the proposal is still being updated as of February [1] so it's still on the table. Your argument with chibicc is actually great, though I personally think typeof is too much (in the same reason I think _Generic and <tgmath.h> is too much: they only solve overly specific problems).

> Also, I'm not sure what 1000 functions you're referring to, [...]

This one was my slight misunderstanding. I should have said 200+ functions, and they mostly come from the updated IEEE 754 standard and additional floating point and decimal types [2]. 1000 was the number of additional reserved identifiers. Still I feel uncomfortable about this sudden explosion in the single global namespace of C.

[1] https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2927.htm

[2] https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2426.pdf#p...


Keeping it simple to write a compiler is not a serious use case these days. People aren't bootstrapping new platforms from scratch. GCC and Clang are anything but simple.


Even if they aren't, people absolutely should be able to bootstrap new platforms from scratch. It's important to have confidence in our tools, in our ability to rebuild from scratch, and to be safe against the "trusting trust" attack among other things.

Lately I've been catching up on the state of the art in bootstrapping. Check out the live-bootstrap project. stage0 starts with a seed "compiler" of a couple hundred bytes that basically turns hex codes into bytes while stripping comments. A series of such text files per architecture work their way up to a full macro assembler, which is then used to write a mostly architecture-independent minimal C compiler, which then builds a larger compiler written in this subset of C. This then bootstraps a Scheme in which a full C compiler (mescc) is written, which then builds TinyCC, which then builds GCC 4, which works its way up to modern GCC for C++... It's a fascinating read:

https://github.com/oriansj/stage0

https://github.com/fosslinux/live-bootstrap/blob/master/part...

Even if no one is "using" this it should still be a primary motivator for keeping C simple.


There are multiple standards for C. C23 does not replace C17, or C11, or C99, or even C89. Maybe something should replace C89, but that's another discussion. I'd vote for C99 as a minimum target and prefer C11.

Just as you yourself described, one can absolutely target a platform from scratch with C99, Forth, Lua, a Pascal, a modernish Basic dialect, or some form of Lisp without building the largest, most modern compiler first. Once you have a compiler or even an interpreter for any of those, one can use it to create a compiler for something bigger and more modern. In fact, that's precisely where C was originally targeted - as a portable bootstrap language to various hardware platforms.

On the other hand, one can also target a new platform via LLVM. Both options have their merits and drawbacks.


> Keeping it simple to write a compiler is not a serious use case these days.

The implication is that if it is simple to write a parser for a language, then the language is simple to read.


Lisp and Scheme seem to discourage this sort of thinking in a great many programmers. After all, Lisps often start as just the eval function for their S-expressions and grow from there.

See also: Brainfuck, Forth, Chinese


Easy != Simple

Linking this makes me nostalgic for 2010 :)

https://www.youtube.com/watch?v=SxdOUGdseq4


Bye bye C.

Bye bye ehhh. Linux, Windows, OS X, Postgres, Android, Python, Sqlite, Redis...


Why not use GObject?


I think writing your own portable C functions is something every programmer wants to own at some point in their young career. Then you grow up and realize there are entire organizations that have spend tens of thousands of man-hours providing the same code, but done correctly and with intent (and consistent philosophy). GObject is that philosophy, but lots of people never hear about it until someone says "Why not use GObject?" :)


This is completely anecdotal, but when I was looking for something like this ~10 years ago (and was just beginning to learn about the wider Linux ecosystem outside of libc), I stumbled upon GObject and completely ignored it because of its association with GNOME. My initial thoughts was that GObject was only really useful if you were building things for the GNOME ecosystem and wrote it off completely. I also did the same with GLib.

I know now that the association only really goes as far as who maintains it, but if it wasn't for how it was branded, I probably would have been using it in a lot more of my projects.

Sounds kind of stupid, but never underestimate how branding can affect how your project gets used. Especially by people who are unfamiliar with it.


I was referring to myself too in my original post, but I don't think it was clear. I resisted GObject/GLib for the exact same reasons you listed. Plus as a younger programmer I was super fussy about style conventions and didn't like introducing other styles with my "pure" style. (I know, I know.) It wasn't until I started using GStreamer that I saw the light.


Your comment could have been mine. Many years ago, I found GObject interesting, but was afraid using it would lead to friction when used outside GNOME's ecosystem.


Perhaps because it's licensed as LGPL, whereas this repo is BSD?


GObject, the C++ implementation by a True C Believer.


tried to use it for embedded system in the past, pretty large in size and impossible to use there, it's in fact larger than c++ stdlib and I ended up going straight to c++ instead.


C++ probably put the smarts in the compiler


GObject's object model is insanely overcomplicated. It's more or less an implementation of C++ in C. It's overkill if you just want resizable arrays, hash tables, and maybe some trees.


I think gp confused gobject with glib. Glib has no object model, it’s a collection of functions and data structures, including “managed” strings. Just a better version of libc, if you don’t mind the size.

https://docs.gtk.org/glib/#structs




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: