Introduction
This article is sort-of a follow-up to my Musings on C & C++ Declarations and Why C Requires the “struct” Keyword for Structures articles specifically focusing on structure (struct) declarations. As a preview, all of the following are valid uses of struct (independently, but not necessarily consecutively in the same program):
struct point; // 1
struct point { int x, y; }; // 2a
struct point { int x, y; } p; // 2b
struct { int x, y; } p; // 3
typedef struct { int x, y; } point; // 4
typedef struct point { int x, int y; } point; // 5a
typedef struct point_s { int x, int y; } point; // 5b
typedef struct point { int x, int y; } point_t; // 5c
1. Forward Structure Declaration
A declaration like:
struct point; // Forward declaration.
is a forward declaration that effectively tells the compiler “point is a structure” but without any other details. What use is that? It allows you to:
- Create an opaque type.
- Break bidirectional type dependencies.
An opaque type can be used as part of an API for a library where you want to keep the details of a structure hidden from the user. This has two benefits:
- It prevents the user from messing around with the members of the structure when they shouldn’t.
- It makes upgrading a library much simpler.
Consequently, users are forced to use only your API to create, manipulate, and destroy objects of such a type. It allows you as the library maintainer to add, remove, or change its members at will without fear of breaking users’ programs. It also allows users to upgrade the version of your library that they use as a drop-in replacement of the shared object file (.so) without having to recompile their programs. This is often very useful in live, production systems.
However, opaque types aren’t without caveats:
All objects must be dynamically allocated. This is both slower and more taxing on the memory allocator, especially for many small objects. (However, you might be able to use an arena.)
No functions operating on an object of the type can be
inlinethat may yield worse performance.
A forward declaration can also be used to break interdependencies. For example, in cdecl, an AST node for a declaration can have an optional alignment; that alignment can be given by a another AST node:
struct c_alignas {
// ...
union {
// ...
struct c_ast *type_ast; // Forward declaration.
};
};
struct c_ast {
c_alignas align; // Alignment, if any.
// ...
};
Unlike C++, a stand-alone forward declaration for a structure isn’t actually needed most of the time since the mere act of mentioning a struct type implicitly creates a forward declaration for it. Above, inside c_alignas, the struct declaration simultaneously:
- Forward-declares
c_astas a structure; and: - Also declares
type_astto be a pointer to such a structure.
2. Ordinary Structure Declaration
A declaration like:
struct point {
int x, y;
};
is just an ordinary structure declaration; a declaration like:
struct point {
int x, y;
} p; // Simultaneously declare 'p' as point.
simultaneously:
- Declares an ordinary structure; and:
- A variable of that structure.
Personally, I don’t like doing this since it’s easy to miss the declaration of the variable way down at the bottom. For readability, I’d rather be a bit redundant by using separate declarations:
struct point {
int x, y;
};
struct point p; // Separate declaration is clearer.
3. Unnamed Structure Declaration
Perhaps curiously, struct can be used without a tag name:
struct { int x, y; } p;
That declares an unnamed structure that’s effectively a one-off. Note that an unnamed structure is not the same as an anonymous structure.
Although unnamed structures by themselves are allowed by the grammar, the only use I can think of for them is declaring multiple variables in a for loop initialization clause. Additionally, two identical unnamed structures are considered different types:
struct { int x, y; } q;
q = p; // Error: different types.
4. typedef Unnamed Structure Declaration
The only place an unnamed structure is useful is when it’s in combination with a typedef:
typedef struct {
int x, y;
} point;
Such declarations are common in C code because they bypass the odd tags namespace and directly make structures be first class citizen types.
Personally, I don’t like doing this since it hides the name of the structure way down at the bottom just like the variable in #2 above. As above, I’d rather be a bit redundant by using separate declarations:
typedef struct slist slist;
struct slist {
slist *next;
void *data;
};
You can put the typedef first since, as stated in #1 above, the mere act of mentioning a struct type implicitly creates a forward declaration for it.
The advantage of putting the typedef first is that you can then use it in the definition of the structure itself as is common in self-referential structures like the singly linked list shown above.
5. typedef Structure Declaration
The last three declarations:
typedef struct point { int x, int y; } point; // 5a
typedef struct point_s { int x, int y; } point; // 5b
typedef struct point { int x, int y; } point_t; // 5c
combine typedef with that of a named structure into the same declaration:
- Case (a) just uses the same name for the tag as the type.
- Case (b) appends
_sto the tag (“s” forstruct). (I’ve seen some codebases do this.) - Case (c) appends
_tto the type. (I’ve seen other codebases do this — even though you really shouldn’t since the_tsuffix is reserved for use by POSIX.)
Consistent with case #4 above, I personally don’t like doing any of these either for the same reason. Again, I’d prefer separate declarations.
Unions & Enumerations
Everything written above also applies to both unions and enumerations with the exception that enumerations can’t be forward declared (until C23 and with fixed-type enumerations).
C++
As mentioned previously, C++ effectively does an implicit typedef for every structure declaration. To keep compatibility, C++ allows all the ways in which a structure can be declared and referred to in C also:
struct point { int x, y; };
point p1; // Ordinary C++ declaration.
struct point p2; // C declaration for compatibility.
Conclusion
Due to the quirky tags namespace, there are several ways to declare structures that has led to inconsistencies across codebases. For your own code, pick one style and use it consistently.
Top comments (0)