This is in regards to some old (pre-C89) code that I'm working on. Part of that code is a small library that defines these structures in the header file:
// lib.h
struct data_node {
const struct data_node *next;
const struct data_node *prev;
void *data;
};
struct trace_node {
const struct trace_node *next;
const struct trace_node *prev;
unsigned int id;
const char *file;
int line;
};
const struct trace_node *get_trace(void);
The source file redefines those same structures, like so:
// lib.c
// does *not* include "lib.h"
struct data_node {
struct data_node *next;
struct data_node *prev;
void *data;
};
struct trace_node {
struct trace_node *next;
struct trace_node *prev;
unsigned int id;
const char *file;
int line;
struct data_node *syncData; /* not included in header file version */
};
It works like you would expect: the syncData
field is not visible to client code that includes the "lib.h" header.
Background
The library maintains 2 internal lists: the trace list, and the data list. The syncData
field keeps the 2 lists in sync (go figure).
If client code had access to the syncData
field, it could disrupt the synchronization between the lists. But, the trace list can get pretty large, so rather than copying every node into a smaller version of the struct, it just returns the address of the sentinel node for the internal list.
Question
I've compiled this with -Wall, -Wpedantic, and -Wextra, and I can't get gcc to complain about it, both with -std=c99 and -std=c11. A hex dump of the memory shows the bytes for the hidden field, right where they ought to be.
The relevant section of the standard (6.2.7.1) says:
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.5 for declarators.46) 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 complete types, 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, and such that if one member of a corresponding pair is declared with a name, the other member 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.
Which, depending on how you want to read it, could be taken to say that compatible struct definitions are restricted to ONLY having corresponding pairs of members (and no others), or that struct definitions are compatible if, where they do have corresponding pairs of members, those pairs meet the requirements.
I don't think this is Undefined Behavior. At worst, I think it may be unspecified. Should I refactor this to use 2 distinct struct definitions? Doing this would require a performance hit to allocate a new public node for each node in the internal list and copy over the public data.