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.
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?
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.
Wait, seriously? They are unique, sure, but the 6.2.7.1 quite explicitly states they are compatible as long as they're in separate files:
Moreover, two structure,
union, or enumerated types declared in separate translation units are compatible if their
tags and members satisfy the following requirements: If one is declared with a tag, the
other shall be declared with the same tag. If both are completed anywhere within their
respective translation units, then the following additional requirements apply: there shall
be a one-to-one correspondence between their members such that each pair of
corresponding members are declared with compatible types; if one member of the pair is
declared with an alignment specifier, the other is declared with an equivalent alignment
specifier; and if one member of the pair is declared with a name, the other is declared
with the same name. For two structures, corresponding members shall be declared in the
same order. For two structures or unions, corresponding bit-fields shall have the same
widths. For two enumerations, corresponding members shall have the same values.
No, you are right, but it is not "in separate files" but "translation units". Two distinct list(int) from two different libraries need to be compatible when used together, which means you would include the header of those two libraries and then both end up in the same translation unit.
C23 relaxed requirements for types with tag, this works for structures with tag, but for type generic structures you then need to synthesize a tag that depends on the type, e.g. list_int, list_float, etc.. so that differently parametrized types do not collide. This works quite well in practice, but is not perfect.
I don't think that's the scenario the other commenter was talking about:
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.
So, lib_a.c includes header_only_list.h, and lib_b.c also includes the same header_only_list.h, but they can't they pass the list structures between each other because those structs would be incompatible even though they're textually identical ("the generated types will be isomorphic but incompatible"). To which I replied that no, they would not, no C program would be able to work if this were true.
Otherwise, the mentioning of monomorphization doesn't make any sense: of course two completely different implementations of lists will be incompatible.
But "the generated types will be isomorphic but incompatible" is right. It is ok if you have lib_a.c and lib_b.c because these are two separate translation units. But this only works if both of these libraries use the list type internally and pass the pointer to each other as a void pointer. The moment you have two headers lib_a.h lib_b.h both including header_only_list.h and defining an API using a generic type list(int) and where you then try to also include lib_b.h from lib_a.c it does not work.
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.
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.
Furthermore, two structure, union, or enumerated types declared in separate translation units are
compatible in the following cases:
— both are declared without tags and they fulfill the preceding requirements;
the preceding requirements being
— there shall be a one-to-one correspondence between their members such that each pair of
corresponding members are declared with compatible types;
— if one member of the pair is declared with an alignment specifier, the other is declared with an
equivalent alignment specifier;
— and, if one member of the pair is declared with a name, the other is declared with the same
name.
For two structures, corresponding members shall be declared in the same order. For two unions
declared in the same translation unit, corresponding members shall be declared in the same order. For
two structures or unions, corresponding bit-fields shall have the same widths. For two enumerations,
corresponding members shall have the same values; if one has a fixed underlying type, then the
other shall have a compatible fixed underlying type. For determining type compatibility, anonymous
structures and unions are considered a regular member of the containing structure or union type,
and the type of an anonymous structure or union is considered compatible with the type of another
anonymous structure or union, respectively, if their members fulfill the preceding requirements.
Seems to me that those two structs satisfy all of those requirements, so they're compatible. Otherwise, struct declarations in the header files would've been completely useless from the very beginning.
Comments
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.
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?
Oh, that's simple: just have empty struct tags.
The names introduced by the typedefs are irrelevant.
Huh?
The structs list_of_A and list_of_B are compatible.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).
Wait, seriously? They are unique, sure, but the 6.2.7.1 quite explicitly states they are compatible as long as they're in separate files:
Did C23 tighten the requirements?No, you are right, but it is not "in separate files" but "translation units". Two distinct list(int) from two different libraries need to be compatible when used together, which means you would include the header of those two libraries and then both end up in the same translation unit.
C23 relaxed requirements for types with tag, this works for structures with tag, but for type generic structures you then need to synthesize a tag that depends on the type, e.g. list_int, list_float, etc.. so that differently parametrized types do not collide. This works quite well in practice, but is not perfect.
I don't think that's the scenario the other commenter was talking about:
So, lib_a.c includes header_only_list.h, and lib_b.c also includes the same header_only_list.h, but they can't they pass the list structures between each other because those structs would be incompatible even though they're textually identical ("the generated types will be isomorphic but incompatible"). To which I replied that no, they would not, no C program would be able to work if this were true.
Otherwise, the mentioning of monomorphization doesn't make any sense: of course two completely different implementations of lists will be incompatible.
But "the generated types will be isomorphic but incompatible" is right. It is ok if you have lib_a.c and lib_b.c because these are two separate translation units. But this only works if both of these libraries use the list type internally and pass the pointer to each other as a void pointer. The moment you have two headers lib_a.h lib_b.h both including header_only_list.h and defining an API using a generic type list(int) and where you then try to also include lib_b.h from lib_a.c it does not work.
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.
It depends what is meant by "compatible." Is the memory layout the same? Yes. Can I memcpy between them? Yes...
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.
Now if I include both library headers in code that attempts to plug them together, the types will be incompatible.
Although not a proof in itself, GCC and Clang seem to agree: https://godbolt.org/z/1ocr5Go5b.
Yes, I figured that's what you meant... I wasn't sure about the other guy.