Live data from Hacker News

I write type-safe generic data structures in C

danielchasehooper.com

71–80 of 196 posts

Re: I write type-safe generic data structures in C

#71

Earlier quoted context omitted.

As you surely know if you're quoting the standard, it depends on which standard!

I believe that since C23 foo() is now a nullary function. As this is the last approved standard and it supersedes all previous standards, it is technically correct to say that de-jure this is what the (unqualified) C standard mandates. Of course de-facto things are more nunanced.

C23 does not change anything in this situation, because we are talking about the definition of main(), not a forward declaration. More details here:

https://news.ycombinator.com/item?id=38729278#38732366

Re: I write type-safe generic data structures in C

#72
post #56

Earlier quoted context omitted.

Thanks, this post is about C. On some projects you must use C.

If I may may be provocative :-) this post isn't about C. It's about layering on a custom language using C preprocessor macros. My compilers were originally written in C. I started using the C preprocessor to do metaprogramming. After some years I got fed up with it and removed nearly all of the preprocessor use, and never looked back. My code was much easier to understand. An amusing story: long ago, a friend of mine…

If the C compiler accepts it, it is C.

Re: I write type-safe generic data structures in C

#73
post #45

Earlier quoted context omitted.

> it should be `int main() { List(Foo) foo_list = {NULL};` In C `int main()` means the function takes an unknown number of arguments. You need `int main(void)` to mean it doesn't take any arguments. This is a fact frequently forgotten by those who write C++.

That had been harmonized with C++ in C23 (e.g. func() is equivalent with func(void) now). It's not really relevant for main() though, even in older C versions main() works fine and simply means "I don't need argc and argv".

This is about a function definition, not a random function declarator. C23 does not change anything in that case.

Re: I write type-safe generic data structures in C

#74
post #12

For your level 2 code, `uint64_t data[];` is wrong for types whose alignment is greater than `uint64_t`, and also wasteful for types whose alignment is smaller (for example, under an ilp32 ABI on 64-bit architectures). For your level 3 code, it should be `int main() { List(Foo) foo_list = {NULL};` Note that working around a lack of `typeof` means you can't return anything. Also, your particular workaround allows `con…

[deleted]

Re: I write type-safe generic data structures in C

#76
post #63

Earlier quoted context omitted.

Any established C codebase, for example the kernel or Postgres? Traditionally microcontroller firmwares as well, though those are increasingly friendly to C++, you just have to be careful about allocations as C++ makes it way easier to accidentally allocate than C does.

I'm not sure about other compilers, but compiling C code as C++ with MSVC ends up with pretty much the exact same code, instruction by instruction. C++ is a bit more strict though especially with casting, so a lot of code won't compile out of the box.

C++ code compiles to a different function names in object file (name mangling). You probably need to put a lot of ugly `#ifdef __cplusplus extern "C" {` boilerplate in your headers, otherwise C and C++ files will not compile together.

Re: I write type-safe generic data structures in C

#77

Why would you jump through all these hoops instead of just writing C++ if you want "C with generics"

because i work on a legacy project that is coupled to safety regulations and other quality guarantees, and we cannot just simply roll out a solution ported to c++ on the next release, or even tenth, so perhaps we make it work until we can.

however we can set a standard and expectation for new projects to use c++, and we do and set an expectation to target a specific std.

i see this sentiment quite a lot on hackernews -- feels like a lot of people saying "git gud" -- i would expect a lot more nuance applied here.

Re: I write type-safe generic data structures in C

#78
post #34
post #31

Earlier quoted context omitted.

Because for many of the use cases where C is used, switching to C++ involves jumping through even more hoops.

Do you have a couple of real world examples?

literally a good majority of existing embedded software coupled to applications in safety -- devices used by fire safety and first responders.

Re: I write type-safe generic data structures in C

#79
I think the idea of using a union to store the element type without any extra run-time memory cost might have some use, specifically in cases where the container struct wouldn't typically store a variable of the element type (or, more likely, a pointer to the element's type) but we want to slip that type information into the struct anyway.

However, the problem that I have with this idea as a general solution for generics is that it doesn't seem to solve any of the problems posed by the most similar alternative: just having a macro that defines a struct. The example shown in the article:

    #define List(type) union { \
        ListNode *head; \
        type *payload; \
    }
could just as easily be:

    #define List(type) struct { \
        type *head; \
        /* Other data, such as node/element count... */ \
    }
(As long as our nodes are maximally aligned - which they will be if they're dynamically allocated - it doesn't matter whether the pointer we store to the list head is ListNode *, type *, void *, or any other regular pointer type.)

The union approach has the same drawback as the struct approach: untagged unions are not compatible with each other, so we have to typedef the container in advance in order to pass in and out of functions (as noted in the article). This is broadly similar to the drawback from which the "generic headers" approach (which I usually call the "pseudo-template" approach) suffers, namely the need for boilerplate from the user. However, the generic-headers/pseudo-template approach is guaranteed to generate the most optimized code thanks to function specialization[1], and it can be combined with another technique to provide a non-type-prefixed API, as I discuss here[2] and demonstrate in practice here[3].

I'd also like to point to my own approach to generics[4] that is similar to the one described here in that it hides extra type information in the container handle's type - information that is later extracted by the API macros and passed into the relevant functions. My approach is different in that rather than exploiting unions, it exploits functions pointers' ability to hold multiple types (i.e. the return type and argument types) in one pointer. Because function pointers are "normal" C types, this approach doesn't suffer from the aforementioned typedef/boilerplate problem (and it allows for API macros that are agnostic to both element type/s and container type). However, the cost is that the code inside the library becomes rather complex, so I usually recommend the generic-headers/pseudo-template approach as the one that most people ought to take when implementing their own generic containers.

[1] https://gist.github.com/attractivechaos/6815764c213f38802227...

[2] https://github.com/JacksonAllan/CC/blob/main/articles/Better...

[3] https://github.com/JacksonAllan/Verstable

[4] https://github.com/JacksonAllan/CC

Re: I write type-safe generic data structures in C

#80
post #63

Earlier quoted context omitted.

I'm not sure about other compilers, but compiling C code as C++ with MSVC ends up with pretty much the exact same code, instruction by instruction. C++ is a bit more strict though especially with casting, so a lot of code won't compile out of the box.

C++ code compiles to a different function names in object file (name mangling). You probably need to put a lot of ugly `#ifdef __cplusplus extern "C" {` boilerplate in your headers, otherwise C and C++ files will not compile together.

Don't forget the infamous pattern used in some C projects too:

  struct foo decl = {
    .member = /* ... */
    .next = &(struct nested_pointer) {
        .nested_member = /* ... */,
    },
    .array = (struct nested_array[]) {
      [0] = { /* ... */ },
    }
  };
This pattern does not work in C++ as the nested declarations become temporaries.
Post reply on HN