The C tag namespacewhy struct S and S can be different things
C keeps struct, union and enum tags in a name space of their own. That is why struct S and an int called S can live in the same file, and why typedef struct S S; is not the tautology it looks like. C23 added two more name spaces, so the count most references give you is now out of date.
#How many name spaces
Four, if you are reading C11. Six, if you are reading C23. The two extras exist because attributes needed somewhere to live.
C11 6.2.3 lists these, and an identifier can be reused across them with no conflict:
- Label names, disambiguated by the syntax of the label declaration and use.
- Tags, for
struct,unionandenum. All three share one tag space. - Member names, one space per struct or union type.
- Ordinary identifiers: variables, functions, typedef names, enumeration constants.
C23 6.2.3 keeps those four and inserts two more between members and ordinary identifiers:
- Standard attributes and attribute prefixes, disambiguated by the syntax of the attribute specifier.
- The trailing identifier in an attribute prefixed token. Each prefix gets its own space for the implementation-defined attributes it introduces, so
gnu::andclang::can both define an attribute of the same name.
Which means this compiles, and the two gnu tokens have nothing to do with each other:
int gnu = 7; /* ordinary identifier */
struct [[gnu::deprecated]] G { int a; }; /* attribute prefix */
int main(void) { return gnu; }Clean under gcc -std=c2x -Wall on GCC 11.4.0. The same holds for int deprecated = 1; sitting next to [[deprecated]].
Note which pairs share a space. All three tag kinds share one, so struct S and union S collide. Typedef names sit with variables in the ordinary space, not with tags. Most of the confusing cases follow from those two facts.
#Tags and variables sharing a name
This compiles cleanly under -std=c11 -Wall -Wextra:
struct S { int x; };
int S = 7; /* ordinary identifier */
struct S s = { .x = 1 }; /* tag, resolved by the struct keyword */
int main(void) { return S + s.x; }The keyword is what disambiguates. struct S can only be a tag lookup, so the int named S never gets in the way. POSIX leans on this: struct stat and the function stat() coexist for exactly this reason, as do struct sigaction and sigaction().
Collisions inside the tag space are diagnosed, and GCC names the reason rather than calling it a redefinition:
struct S { int x; };
union S { int y; };
/* error: 'S' defined as wrong kind of tag */
struct S { int x; };
enum S { A };
/* error: 'S' defined as wrong kind of tag */#Why typedef struct S S works
The idiom looks circular and is not:
typedef struct S S; /* ordinary 'S' = the type; tag 'S' unchanged */
struct S { int x; }; /* tag completed afterwards */
int main(void) {
S a = { 1 };
struct S b = { 2 };
return a.x + b.x;
}The typedef introduces S into the ordinary space and points it at the type named by tag S. Two different spaces, two different entries, one spelling. Both declarations above name the same type.
The forward reference is legal too. typedef struct S S; before struct S has a body declares an incomplete type, which is enough for pointers. This is how self-referential types get written without repeating the struct keyword everywhere:
typedef struct Node Node;
struct Node { int v; Node *next; };#When S and struct S are different types
Here is the case that surprises people. Both of these are valid at once:
struct S { int x; };
typedef int S; /* ordinary 'S' = int; tag 'S' still the struct */
int main(void) {
S i = 1; /* an int */
struct S v = { 2 }; /* the struct */
return i + v.x;
}GCC 11.4.0 accepts this under -std=c11 -Wall -Wextra with no diagnostic. S and struct S now name unrelated types, and a reader skimming the file has no signal that the two spellings diverge. Nothing in C prevents it, so a header that does this is legal and hostile at the same time.
#C23 attributes on a tag
C23 allows an attribute specifier between the keyword and the tag:
union [[deprecated]] U { int a; };The attribute sits after union and before the tag name, which is a position a parser looking for an identifier immediately after the keyword will not expect. Tools that scan for tag declarations by pattern rather than by parsing tend to break here first.
Two details worth having. First, GCC 11.4.0 accepts the syntax under -std=c11 as well as -std=c2x; only -pedantic objects, with warning: ISO C does not support '[[]]' attributes before C2X. So the construct will turn up in code that never opted into C23. And GCC applies the attribute rather than parsing and discarding it: using the tag afterwards produces warning: 'T' is deprecated [-Wdeprecated-declarations].
Second, the attribute is only legal on the form that carries a member list. C23 6.7.3.4p3 states that a type specifier of the form struct-or-union attribute-specifier-sequenceopt identifier, meaning the referencing form with no body, shall not contain an attribute specifier sequence:
struct [[deprecated]] T { int a; }; /* defining form: legal */
struct T { int a; };
struct [[deprecated]] T t; /* referencing form: constraint violation */
/* error: expected ';' before 't' */#C23 changed tag compatibility
Through C17, two definitions of struct P in one translation unit were a redefinition error, and two definitions in different translation units produced distinct but compatible types. N3037 changed the first half. Under C23 6.7.3.4p1 and 6.2.7p1, repeating a tag definition inside one translation unit is legal as long as the two definitions agree: one-to-one corresponding members, same names, same types rather than merely compatible ones, matching alignment specifiers, and neither nested inside the other.
That makes header-generated types practical. A macro that emits struct vec_int { int *data; size_t len; }; can now be expanded twice in one file without the second expansion failing.
Toolchain support lags the standard. GCC 11.4.0 rejects the duplicate under -std=c2x with error: redefinition of 'struct P', the same as under -std=c11. Clang implemented N3037 in llvm-project#132939. Check your compiler before relying on it.
What did not change: incomplete types stay incompatible within a translation unit, and a type being redeclared is incompatible with itself until the declaration ends.
#What this means in practice
Three rules cover almost every case:
- A tag is only ever reachable through
struct,unionorenum. If those keywords are absent, you are in the ordinary space. - All three tag kinds share one space.
struct Sandenum Sin one scope is a wrong-kind-of-tag error, not two tags. - A typedef name is an ordinary identifier and can be shadowed by a local variable or a parameter. Once shadowed,
S *x;in that scope parses as multiplication rather than a pointer declaration.
That last one is the classic C parsing ambiguity, and both readings of the identical token sequence are valid C:
typedef int S;
int f(void) { S * x; return 0; } /* declares a pointer */
int g(int S) { int x=0; S * x; return 0; } /* multiplies S by x */GCC 11.4.0 confirms the split by warning about different things on the two lines: unused variable 'x' on the declaration, statement with no effect on the expression. Deciding which is which requires tracking every typedef and every shadow at every scope depth, which is why C cannot be parsed without a symbol table feeding the parser. The workaround has a name, the lexer hack, and it is the reason C parsers cannot be a clean pipeline of lexer then parser.
#How Prism handles it
Prism registers every typedef, enum constant, VLA tag and parameter shadow at all scope depths before it emits any output, because it has to resolve exactly the ambiguity above to know whether S * x; is a declaration. Getting tag lookup wrong is one of the ways a C transpiler silently miscompiles rather than failing loudly.
If you are writing a tool that reads C rather than using one, the takeaway is the same either way: tag lookup and ordinary lookup are separate tables, a shadow in one does not affect the other, and since C23 there are four more tables than the two you were thinking about.
#Further reading
- ISO/IEC 9899:2024 (C23), N3220 working draft: 6.2.3 (name spaces), 6.2.7 (compatible type), 6.7.3.4 (tags).
- WG14 N3037: Improved Rules for Tag Compatibility, the paper behind the C23 change.
- cppreference: lookup and name spaces, which marks the two attribute spaces "since C23" and has a worked example using one identifier in four of them.
- Chris Wellons, "Parameterized types in C using the new tag compatibility rule", on what N3037 makes possible.
Every code sample on this page compiled on GCC 11.4.0, Ubuntu 22.04. Standard text quoted from ISO/IEC 9899:2024 (N3220) and ISO/IEC 9899:2011.