| > I do not think a type-safe macro-generated list in C is any more awkward to implement or inferior to a C++ template version. I have some experience with this, and while this is one of these things that are feasible, I find them significantly inferior to templates in practice. For one thing, footguns are everywhere in C macro-based metaprogramming. E.g. do not declare the payload as 'payload_type payload;' in the node structure, but choose 'typeof(payload_type) payload;' instead, as someone may pass an array type or function pointer type for 'payload_type'. Speaking of array types, how do you deal with the fact that you cannot pass them by value? I will choose C++ templates' semantic substitution over C macros' textual substitution. Anyway, to me, the biggest limitation of macro-generation compared to templates is that there is no centralized monomorphization. If an application uses two libraries, each of which handles lists of int, each library will have to independently macro-generate its separate list implementation, and because C's type system is nominal, the generated types will be isomorphic but incompatible. Contrast this with C++ templates, where two independent libraries can happily share std::list<int> values. |
| |
| ▲ | uecker 3 hours ago | parent | next [-] | | You are right that it is not perfect, but it is fine for me and usability is not worse than for C++. I use the rule that only identifiers (typedef names) can be passed. Then the macro can synthesize a tag and list type is then compatible between different libraries. It could look like this:
https://codeberg.org/uecker/noplate/src/branch/main/tests/li... The predeclarations are not needed anymore in C23 and I hope for the next version of C we can also get rid of the limitation that an identifier needs to be passed to the macro (by making the type system fully structural). | |
| ▲ | Joker_vD 4 hours ago | parent | prev [-] | | > If an application uses two libraries, each of which handles lists of int, each library will have to independently macro-generate its separate list implementation, and because C's type system is nominal, the generated types will be isomorphic but incompatible. Um, what? C89, 3.1.2.6: "Moreover, two structure, union, or enumeration types declared in separate translation units are compatible if they have the same number of members, the same member names, and compatible member types; for two structures, the members shall be in the same order". There has been some minor changes over the years, but as long as the struct tags are the same, and the fields are in the same order and have compatible types, the two structs defined in separate compilation units are compatible. | | |
| ▲ | el_pollo_diablo 3 hours ago | parent [-] | | > as long as the struct tags are the same Exactly. Now you have a naming problem. You need a naming convention that every user of the list library must follow, or else their types will be incompatible. And what about typedefs? If A is a typedef of B, or more generally A and B are typedef-related (their normal forms, obtained by following all typedefs, are the same), lists of A and B will be incompatible unless users agree on a common name. The only realistic choice is the normal form, but then this actively works against the abstraction provided by typedef. And this is just for types. What about functions? While it is legal to do identical definitions of struct list_int, it is not for list_int_init() and list_int_add(). Or global variables: it is legal to do several identical extern declarations, but there can only be one definition; which compilation unit gets to do it? | | |
| ▲ | Joker_vD 3 hours ago | parent [-] | | > Now you have a naming problem. You need a naming convention that every user of the list library must follow, or else their types will be incompatible. Oh, that's simple: just have empty struct tags. > And what about typedefs? The names introduced by the typedefs are irrelevant. > A and B are typedef-related (their normal forms, obtained by following all typedefs, are the same), lists of A and B will be incompatible unless users agree on a common name. Huh? typedef struct { int x; } A;
typedef struct { int x; } B;
typedef struct { header_list header; A payload; } list_of_A;
typedef struct { header_list header; B payload; } list_of_B;
The structs list_of_A and list_of_B are compatible. | | |
| ▲ | uecker 2 hours ago | parent | next [-] | | No, any tagless type is unique, so neither A and B nor list_of_A and list_of_B are compatible. This is what I like to fix in C2y outside of typedefs (and it would really help if you file wishlist bugs with compilers if you agree). | |
| ▲ | el_pollo_diablo 2 hours ago | parent | prev [-] | | > The structs list_of_A and list_of_B are compatible. No, they are not. From C23, 6.7.3.4 Tags: Each declaration of a structure, union, or enumerated type which does not include a tag declares a distinct type. | | |
| ▲ | icedchai 2 hours ago | parent [-] | | It depends what is meant by "compatible." Is the memory layout the same? Yes. Can I memcpy between them? Yes... | | |
| ▲ | el_pollo_diablo an hour ago | parent [-] | | We mean compatible as defined by the C language standard. It is much more restrictive than having the same layout. In particular, you may not pass a pointer to a type where a pointer to an incompatible type is expected, even if the types have the same layout, which prevents the sort of sharing between two libraries that is being discussed. Moreover, there is no guarantee that two distinct structure types with the same list of members have the same size or alignment (although in practice they do). The members must nevertheless be laid out in the same way (same offsets, and in the case of bit-fields, same layout inside storage units) due to an obscure constraint on common initial sequences. So the layouts of the structures may differ in the alignment requirement and the amount of trailing padding. | | |
| ▲ | icedchai 9 minutes ago | parent [-] | | Yes, I figured that's what you meant... I wasn't sure about the other guy. |
|
|
|
|
|
|
|