... I learned the former "hybrid" construct
[i.e., "typedef struct tag { ...} alias;"]
from a professional programmer and have
used it in my hobby code for years without a problem. Can you
give an example of it not working right?
Examples are a little tricky, as the worst-case problem occurs
only with what I consider "poor style" in the first place.
Of course, the simplest, most "obvious" problem with:
typedef struct list { /* linked list of key/value pairs */
struct list *next;
int key, value;
} List;
is that we have to write "struct list *next" in the definition
of the structure. If you split it up into:
typedef struct list List;
struct list {
List *next;
int key, value;
};
you get to use your shortcut-name ("List") instead of pressing
the seven keys s t r u c t <spacebar>.
If you have mutually-referential structures, you can use the same
technique, again writing all the "typedef" lines first:
typedef struct S1 S1;
typedef struct S2 S2;
struct S1 {
S1 *next; /* linked list of "S1"s */
S2 *other; /* with a reference to an "S2" */
... data ...
};
struct S2 {
S2 *next; /* linked list of "S2"s */
S1 *other; /* with a reference to an "S1" */
... data ...
};
This works because the first mention of a struct tag, in a scope
in which no such tag is yet visible, defines a new type with that
name -- "struct <whatever>" -- at the current scope. At file scope,
when "struct S1" and "struct S2" are not yet defined, the first
mention occurs inside the combined "typedef struct <tag> <alias>"
line.
If you are, for whatever reason, happy to type out the keyword
"struct" inside the definitions, you can even combine all of the
above into:
typedef struct S1 {
struct S1 *next;
struct S2 *other;
... data ...
} S1;
typedef struct S2 {
struct S2 *next;
S1 *other; /* note: asymmetry */
... data ...
} S2;
Inside a block, however, the picture *can* be different. Consider
the following code:
typedef struct S1 { int x; } S1;
typedef struct S2 { int y; } S2;
void f(void) {
typedef struct S1 {
struct S1 *next;
struct S2 *other;
int s1_member;
} S1;
typedef struct S2 {
struct S2 *next;
S1 *other;
int s2_member;
} S2;
S1 tmp1;
S2 tmp2;
tmp1.other = &tmp2; /* note: this is line 19 */
}
If I feed this code fragment to gcc, I get:
t.c: In function `f':
t.c:19: warning: assignment from incompatible pointer type
Do you see why?
Here, tmp1 is an object of type "struct S1" (or its alias, S1).
It contains a member named "other" which has type "struct S2 *" --
and "tmp2" is an object of type "struct S2" (or its alias S2).
So why can I not set tmp1.other to point to tmp2?
The answer is that these are *different* "struct S2"s. This becomes
a little more obvious if we add one more line (and #include <stdio.h>
as needed):
printf("%d\n", tmp1.other->s2_member);
which produces:
t.c:22: structure has no member named `s2_member'
but if we change that to:
printf("%d\n", tmp1.other->y);
the compiler stops complaining about the printf() line.
To fix this, we have to use a feature added way back in C89,
the "vacuous" structure declaration. Inserting the line:
struct S2;
at the top of f(), before the definition of the new, inner-scope
"struct S1" and its alias, fixes everything:
#include <stdio.h>
typedef struct S1 { int x; } S1;
typedef struct S2 { int y; } S2;
void f(void) {
struct S2;
typedef struct S1 {
struct S1 *next;
struct S2 *other;
int s1_member;
} S1;
typedef struct S2 {
struct S2 *next;
S1 *other;
int s2_member;
} S2;
S1 tmp1;
S2 tmp2;
tmp1.other = &tmp2;
tmp2.s2_member = 42;
printf("%d\n", tmp1.other->s2_member);
}
int main(void) {
f();
return 0;
}
% cc -O -W -Wall -ansi -pedantic -o t t.c
% ./t
42
%
In various specific cases, the vacuous "struct" line can be removed,
but in the general case, writing them all out is always completely
safe.
Note that writing:
typedef struct S1 { int x; } S1;
typedef struct S2 { int y; } S2;
void f(void) {
typedef struct S1 S1;
typedef struct S2 S2;
struct S1 {
S1 *next;
S2 *other;
int s1_member;
};
struct S2 {
S2 *next;
S1 *other;
int s2_member;
};
...
}
does *not* work, because the inner "typedef" lines refer back to
the existing "struct S1" and "struct S2", so that the aliases in
f() still refer to the outer "struct S1" and outer "struct S2".
This is presumably not what the programmer had in mind.
(Note that you have to use the vacuous definition of "struct S2"
even if you avoid the "typedef" keyword, whenever you write code
that shadows identifiers like this. For some reason, it "looks
less odd" to me than the version with the typedefs, though.)