Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does the C standard consider that there are one or two 'struct uperms_entry' types in this header?

Tags:

c

c99

Can you give chapter and verse from one of the three C standards (preferably C99 or C11) which indicates whether the following header file has one or two struct uperms_entry types in it?

#ifndef UPERMS_CACHE_INCLUDE
#define UPERMS_CACHE_INCLUDE

typedef struct mutex MT_MUTEX;

typedef struct uperms_cache
{
    MT_MUTEX            *cache_lock;
    int                  processing;
    struct uperms_entry *uperms_list;  // No prior struct uperms_entry
} uperms_cache_t;

typedef struct uperms_entry // Does this define a different struct uperms_entry?
{
    char                 username[32];
    int                  perms;
    struct uperms_entry *next;
} uperms_entry_t;

#endif /* UPERMS_CACHE_INCLUDE */

Adjunct questions:

  1. If there are two types, is there any way to get GCC to report the issue?
  2. If there are two types, does it ever matter in practice?

(I think the answers are 'yes — strictly there are two types', and then (1) No and (2) No.)

Context: internal code review — I'd like the order of the structures reversed, but I'm not sure whether I'm being completely overly pedantic.

Update:

Clearly, the answer to the initial question is 'there is one struct uperms_entry' and therefore the questions numbered 1 and 2 are moot. I'm glad I checked before throwing a hissy fit in a code review.

Background thinking

This section was added long after the primary question was resolved.


Here are some extensive but relevant quotes from ISO/IEC 9899:2011:

§6.2.7 Compatible type and composite type

¶1 Two types have compatible type if their types are the same. Additional rules for determining whether two types are compatible are described in 6.7.2 for type specifiers, in 6.7.3 for type qualifiers, and in 6.7.6 for declarators.55) 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.

55) Two types need not be identical to be compatible.

§6.7.2.1 Structure and union specifiers

¶8 The presence of a struct-declaration-list in a struct-or-union-specifier declares a new type, within a translation unit. The struct-declaration-list is a sequence of declarations for the members of the structure or union. If the struct-declaration-list does not contain any named members, either directly or via an anonymous structure or anonymous union, the behavior is undefined. The type is incomplete until immediately after the } that terminates the list, and complete thereafter.

§6.7.2.3 Tags

¶4 All declarations of structure, union, or enumerated types that have the same scope and use the same tag declare the same type. Irrespective of whether there is a tag or what other declarations of the type are in the same translation unit, the type is incomplete129) until immediately after the closing brace of the list defining the content, and complete thereafter.

¶5 Two declarations of structure, union, or enumerated types which are in different scopes or use different tags declare distinct types. Each declaration of a structure, union, or enumerated type which does not include a tag declares a distinct type.

¶6 A type specifier of the form

struct-or-union identifieropt{ struct-declaration-list }

or

enum identifieropt{ enumerator-list }

or

enum identifieropt{ enumerator-list , }

declares a structure, union, or enumerated type. The list defines the structure content, union content, or enumeration content. If an identifier is provided,130) the type specifier also declares the identifier to be the tag of that type.

¶7 A declaration of the form

struct-or-union identifier ;

specifies a structure or union type and declares the identifier as a tag of that type.131)

¶8 If a type specifier of the form

struct-or-union identifier

occurs other than as part of one of the above forms, and no other declaration of the identifier as a tag is visible, then it declares an incomplete structure or union type, and declares the identifier as the tag of that type.131)

¶9 If a type specifier of the form

struct-or-union identifier

or

enum identifier

occurs other than as part of one of the above forms, and a declaration of the identifier as a tag is visible, then it specifies the same type as that other declaration, and does not redeclare the tag.

¶12 EXAMPLE 2 To illustrate the use of prior declaration of a tag to specify a pair of mutually referential structures, the declarations

struct s1 { struct s2 *s2p; /* ... */ }; // D1
struct s2 { struct s1 *s1p; /* ... */ }; // D2

specify a pair of structures that contain pointers to each other. Note, however, that if s2 were already declared as a tag in an enclosing scope, the declaration D1 would refer to it, not to the tag s2 declared in D2. To eliminate this context sensitivity, the declaration

struct s2;

may be inserted ahead of D1. This declares a new tag s2 in the inner scope; the declaration D2 then completes the specification of the new type.

129) An incomplete type may only by used when the size of an object of that type is not needed. It is not needed, for example, when a typedef name is declared to be a specifier for a structure or union, or when a pointer to or a function returning a structure or union is being declared. (See incomplete types in 6.2.5.) The specification has to be complete before such a function is called or defined.

130) If there is no identifier, the type can, within the translation unit, only be referred to by the declaration of which it is a part. Of course, when the declaration is of a typedef name, subsequent declarations can make use of that typedef name to declare objects having the specified structure, union, or enumerated type.

131) A similar construction with enum does not exist.

§6.7.3 Type qualifiers

¶10 For two qualified types to be compatible, both shall have the identically qualified version of a compatible type; the order of type qualifiers within a list of specifiers or qualifiers does not affect the specified type.

The discussion in §6.7.6 is related to pointer, arrays, and function declarators and does not really affect structures or unions.


I was aware of Example 2 when I wrote the question. This is some thinking out loud about some of what the information above means.

Consider this example, which compiles cleanly:

#include <stdio.h>
struct r1 { int x; };

struct r1;

struct r1 p0;

//struct r1 { int y; };     // Redefinition of struct r1

extern void z(void);

void z(void)
{
    struct r1 p1 = { 23 };
    struct r1;
    //struct r1 p2;         // Storage size of p2 is not known
    struct r2 { struct r1 *rn; int y; };
    struct r1 { struct r2 *rn; int z; };
    struct r2 p = { 0, 1 };
    struct r1 q = { &p, 2 };
    p.rn = &q;
    printf("p.y = %d, q.z = %d\n", p.y, q.z);
    printf("p1.x = %d\n", p1.x);
}

The function illustrates when Example 2 applies, but is not sensible code. The declaration of p1 in the function is would be a structure of the same type as the global variable p0. Even though its type name is struct r1, it is of a different (and incompatible) type from the type of the local variable p.

The redefinition of struct r1 at the global level is not allowed, regardless of whether the element is named x or y. The prior struct r1; is a no-op in this context.

One interesting issue is 'can function z pass p or q to any other function (call it a)? The answer is a qualified 'yes', and some of the constraints are interesting. (It would also be appalling coding style to try it, verging on the insane.) The function must exist in a separate translation unit (TU). The function declaration must be inside function z (because if it is outside the function, its prototype must refer to the struct r1 defined outside the function, not the struct r1 defined inside.

In the other TU, a degree of sanity must prevail: the function a must have the compatible structure types struct r1 and struct r2 visible in its global scope.

Here's another example, but this one does not compile:

#include <stdio.h>

struct r1;
extern void z(struct r1 *r1p);
extern void y(struct r1 *r1p);

void y(struct r1 *r1p)
{
    struct r2 { struct r1 *rn; int y; };
    struct r1 { struct r2 *rn; int z; };
    struct r2 p = { r1p, 1 };
    struct r1 q = { &p, 2 };
    p.rn = &q;
    printf("p.y = %d, q.z = %d\n", p.y, q.z);
}

void z(struct r1 *r1p)
{
    struct r1
    struct r2 { struct r1 *rn; int y; };
    struct r1 { struct r2 *rn; int z; };
    struct r2 p = { r1p, 1 };
    struct r1 q = { &p, 2 };
    p.rn = &q;
    printf("p.y = %d, q.z = %d\n", p.y, q.z);
}

The warnings from GCC 4.7.1 on Mac OS X 10.7.4 are:

structs3.c: In function 'y':
structs3.c:13:10: warning: assignment from incompatible pointer type [enabled by default]
structs3.c: In function 'z':
structs3.c:22:12: warning: initialization from incompatible pointer type [enabled by default]
structs3.c:22:12: warning: (near initialization for 'p.rn') [enabled by default]

Lines 13 is the assignment p.rn = &q; in function y and line 23 is the attempt to define and initialize struct r2 p in function z.

This demonstrates that within the functions, the rn element of struct r2 is a pointer to the incomplete type struct r1 declared at the global scope. Adding a struct r1; as the first line of code inside the function would allow the code to compile, but the initialization referencing r1p->rn is derefencing a pointer to an incomplete type again (the incomplete type is the struct r1 declared at the global scope).

The function declarations and the preceding struct r1; line could appear in a header as an opaque type. The list of supporting functions is incomplete; there'd need to be a way to get a pointer to an initialized struct r1 to pass into the functions, but that's a detail.

To make the code work in this second TU, the types for struct r1 must be complete in the global scope before the functions are defined, and because of the recursive references, `struct r21 must also be complete.

#include <stdio.h>

/* Logically in a 3-line header file */
struct r1;
extern void z(struct r1 *r1p);
extern void y(struct r1 *r1p);

/* Details private to this TU */
struct r2 { struct r1 *rn; int y; };
struct r1 { struct r2 *rn; int z; };

void y(struct r1 *r1p)
{
    struct r2 p = { r1p,     1 };
    struct r1 q = { r1p->rn, 2 };
    p.rn = &q;
    printf("p.y = %d, q.z = %d\n", p.y, q.z);
}

void z(struct r1 *r1p)
{
    struct r2 p = { r1p,     1 };
    struct r1 q = { r1p->rn, 2 };
    p.rn = &q;
    printf("p.y = %d, q.z = %d\n", p.y, q.z);
}

This process of defining the structures in the implementation file while leaving the type incomplete in the public header file can be repeated in multiple implementation files if necessary, though if more than one TU uses the complete structure definition, it would be better to place the definitions in a private header file shared only between the files that implement the structures. I note that it does not matter whether the private header precedes or follows the public header.

Maybe this was all obvious to you already. I'd not needed to think it through in this level of detail before.

like image 479
Jonathan Leffler Avatar asked Feb 19 '23 17:02

Jonathan Leffler


1 Answers

In C1, they refer to the same type. C99 §6.2.1 defines the scopes that exist:

2 For each different entity that an identifier designates, the identifier is visible (i.e., can be used) only within a region of program text called its scope. Different entities designated by the same identifier either have different scopes, or are in different name spaces. There are four kinds of scopes: function, file, block, and function prototype. (A function prototype is a declaration of a function that declares the types of its parameters.)

Function scope only applies to labels (as explicitly stated later in the same section). Block scope applies to identifiers declared in blocks - blocks are created by compound-statements, iteration-statements and selection-statements (and not by struct declarations or compound initialisers). Function prototype scope applies to identifiers declared within a function prototype declaration.

None of these apply to your example - all of the mentions of struct uperms_entry in your example are at file scope.

C99 §6.7.2.3 says:

1 All declarations of structure, union, or enumerated types that have the same scope and use the same tag declare the same type. The type is incomplete until the closing brace of the list defining the content, and complete thereafter.

This is pretty clear, and applies to your case.

Paragraph 8 of that section applies to the first mention of struct uperms_entry:

8 If a type specifier of the form struct-or-union identifier occurs other than as part of one of the above forms, and no other declaration of the identifier as a tag is visible, then it declares an incomplete structure or union type, and declares the identifier as the tag of that type.

So at that point it's declared as an incomplete type at file scope. Paragraph 6 applies to the second mention of struct uperms_entry:

6 A type specifier of the form struct-or-union identifieropt { struct-declaration-list } or enum identifier { enumerator-list } or enum identifier { enumerator-list , } declares a structure, union, or enumerated type. The list defines the structure content, union content, or enumeration content. If an identifier is provided, the type specifier also declares the identifier to be the tag of that type.

So after the } at the end of that typedef declaration, it's now a complete type.

The adjunct questions are moot.


1. I believe that this is not the case in C++, however.
like image 116
caf Avatar answered Apr 08 '23 06:04

caf