8

The fact that a struct with a flexible array member is a type with which a variable can be declared and to which sizeof can be applied leads to an unusual behavior in the following program.

file fam1.c:

#include <stdio.h>
#include <stddef.h>

struct s {
  char c;
  char t[]; };

extern struct s x;

size_t s_of_x(void);

int main(void) {
  printf("size of x: %zu\n", sizeof x);
  printf("size of x: %zu\n", s_of_x());
}

file fam2.c:

#include <stddef.h>

struct s {
  char c;
  char t[2]; };

struct s x;

size_t s_of_x(void) {
  return sizeof x;
}

This program, when compiled and run, emits a somewhat surprising output:

$ clang -std=c17 -pedantic -Wall fam1.c fam2.c
$ ./a.out 
size of x: 1
size of x: 3

Note that you can also move the “extern” to fam2.c, and that makes the program worse in terms of having unexpected behavior if x.t is accessed. To be clear, I don't know if such a variant would be less defined according to the C17 standard, but I am pretty sure that most compilers would generate object files that, when linked together, produce a dysfunctional binary.

I am unsure whether the intent of the C17 standard is make the program made of fam1.c and fam2.c undefined, but I do not see what clauses in it make it so. One might think of C17's clauses 6.2.7:1 and 6.2.7:2, but if you read them carefully, they appear to exactly allow what fam1.c and fam2.c are doing:

6.2.7 Compatible type and composite type

6.2.7: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.

6.2.7:2 All declarations that refer to the same object or function shall have compatible type; otherwise, the behavior is undefined.

For reference, flexible array members are described in 6.7.2.1:18:

6.7.2.1:18 As a special case, the last element of a structure with more than one named member may have an incomplete array type; this is called a flexible array member. In most situations, the flexible array member is ignored. In particular, the size of the structure is as if the flexible array member were omitted except that it may have more trailing padding than the omission would imply. However, when a . (or-> ) operator has a left operand that is (a pointer to) a structure with a flexible array member and the right operand names that member, it behaves as if that member were replaced with the longest array (with the same element type) that would not make the structure larger than the object being accessed; the offset of the array shall remain that of the flexible array member, even if this would differ from that of the replacement array. If this array would have no elements, it behaves as if it had one element but the behavior is undefined if any attempt is made to access that element or to generate a pointer one past it.

Am I missing something, in 6.2.7 or elsewhere in C17, that makes fam1.c+fam2.c undefined? Or is it a defined C program according to the C17 standard, and in that case, is the variant where extern is on the non-FAM version of the struct and x.t is accessed in the same compilation unit defined for the same reason?

(This is a digression, but I think I can explain why 6.2.7:1 is written the way it is. The intent is likely to allow, say, struct s { int (*m)[]; } x; in one compilation unit and struct s { int (*m)[2]; } x; in another)

Pascal Cuoq
  • 79,187
  • 7
  • 161
  • 281
  • 2
    `emits a somewhat surprising output` Why suprising? You are comparing apples to oranges. These are _two different_ struct s. – KamilCuk Aug 26 '22 at 07:43
  • 1
    @KamilCuk The program is not printing the size of any `struct s`. The program is printing the size of the variable `x`. There is only one variable `x`. – Pascal Cuoq Aug 26 '22 at 07:45
  • I'm sorry, [in the comment you have deleted] you refer to “the type” of the variable `x`, but the point of the question is that it seems to have at least two, so I'm not sure what you mean. – Pascal Cuoq Aug 26 '22 at 07:47
  • 1
    I occurs to me that it's possible to interpret “ In most situations, the flexible array member is ignored” as meaning that when asking the question of whether the types of `x` in the respective declarations of fam1.c and fam2.2, the flexible array member should be ignored. This would mean that the program made of these two compilation units does in fact violate 6.2.7:2. – Pascal Cuoq Aug 26 '22 at 07:54
  • 3
    Would it help to recognise that there are two definitions of the struct, one in each compilation unit. One has a member variable that is a VLA, the other has a fixed size array... Try putting the two definitions of the struct together into a shared header file.... The 'extern' is a promise to the compiler that the linker will find a struct containing a VLA array in fam2.o... You are breaking the promise... – Fe2O3 Aug 26 '22 at 08:02
  • 1
    @Fe2O3 I agree I am breaking a promise to the C compiler. I am asking what clause of C17 I am breaking. Because if I'm not breaking any, then the C compiler is breaking the promise it made of compiling defined C programs, too. – Pascal Cuoq Aug 26 '22 at 08:10
  • What happens when you replace `t[]` with `t[1]` ? – Agent_L Aug 26 '22 at 08:12
  • 1
    @Agent_L Then the program breaks `6.2.7:2`, so it's undefined. – Pascal Cuoq Aug 26 '22 at 08:17
  • Myself, I'd go with the `sizeof` that of fam2.c where the actual definition resides... fam1.c could declare `extern double x;` and the compiler has to trust that because it only sees one compilation unit at a time... I can declare I'm a top coder... Declaration and definition are two separate things... much like your two struct declarations... – Fe2O3 Aug 26 '22 at 08:17
  • 2
    @Fe2O3 I should reiterate maybe that the question is not whether an ordinary C compiler can handle the sort of situation I allude to in presence of separate compilation—it clearly can't, or how the program should be written instead—there is no reason to provide a size in some compilation units for FAMs, they are not intended to be used like this. The question is whether there is an oversight in the C standard that makes my example accidentally defined. – Pascal Cuoq Aug 26 '22 at 08:24
  • Refer back to the example about fam1.c declaring `extern double x;`... The compiler did exactly what is expected, reporting the size of the struct (the size of x) that it had been informed about... You tell it lies and you reap the appropriate rewards... I don't want to take on the standards committee... Be my guest... – Fe2O3 Aug 26 '22 at 08:30
  • 1
    @Fe2O3 If I wrote `extern double x;`, the program would be undefined by “6.2.7:2 All declarations that refer to the same object or function shall have compatible type; otherwise, the behavior is undefined”, which I quote in the question. The rest of the question shows that the text of the C standard does not appear to make the actual example in the question undefined this way. – Pascal Cuoq Aug 26 '22 at 08:35
  • 1
    6.2.5:22: "An array type of unknown size is an incomplete type." 6.5.3.4:1: "The sizeof operator shall not be applied to an expression that has function type or an incomplete type" – Agent_L Aug 26 '22 at 08:37
  • @Fe2O3 We're not there yet, because I can't find a rule explicitly making "struct with incomplete type" an incomplete type itself. – Agent_L Aug 26 '22 at 08:39
  • 1
    @Agent_L A struct with a FAM is not an incomplete type. – Pascal Cuoq Aug 26 '22 at 08:39
  • If your fam1.c struct is "complete", then you can take the size of every member of that struct... Try adding `size_t sizeOfMemberT = sizeof x.t;` to fam1.c and see how far you get... I'm outta here... – Fe2O3 Aug 26 '22 at 08:45
  • 1
    I'm struggling to find where it's specified when array types are compatible. Obviously the program is broken because `s` is declared as having different sizes and code compiled under one is not compatible (in the natural sense) with the other. Objects may be overwritten or sliced because different sizes are used. But where is that in the C Standard? – Persixty Aug 26 '22 at 08:45
  • 2
    If a struct with a FAM is a complete type, then you had violated the rules by using 2 conflicting definitions (because only incomplete type can be completed elsewhere). If a struct with a FAM is a incomplete type, then you had violated the rules by using `sizeof`. – Agent_L Aug 26 '22 at 08:46
  • 1
    @Agent_L The question quotes the clause that makes conflicting definitions undefined. That clause uses the notion of “compatible type”, which I also quoted (but only partly because the definition of “compatible type” is spread over several clauses. I have quoted the also-relevant 6.7.6.2:6 clause in a comment below an answer). These clauses taken together do not appear to say that my example is an undefined case, unless you use the interpretation I posted in the fourth comment under the question, which seems to be to be a bit thin as a justification. – Pascal Cuoq Aug 26 '22 at 08:48
  • 1
    Pretty sure that the intent of 6.2.7/1 *"each pair of corresponding members are declared with compatible types"* is simply to allow, for example, a member to be declared as `uint32_t` in one file, and `unsigned int` in another file, provided that `unsigned int` and `uint32_t` are in fact the same type for the given implementation. In other words, the intent is that the two structures are identical, and you're just overthinking it. – user3386109 Aug 26 '22 at 08:51
  • 3
    @user3386109 The intent may be one thing or the other. I'm not overthinking anything, I wrote a program and I am asking if it is defined according to the C standard. It's a simple question, but “you're overthinking it” does not seem to be an answer to it. – Pascal Cuoq Aug 26 '22 at 08:53
  • 2
    I have to concede. It seems to be a loophole in 6.7.6.2:6 - it hinges upon 6.7.6.2:4 but requirement of incompleteness isn't stated explicitly. And you've circumvented incompletness with FAM. It goes away if we look back to prehistory of FAM, where the last member was declared with size zero, and clearly that's the intent the FAM is defined with. – Agent_L Aug 26 '22 at 09:07
  • 2
    Regardless of the C Standard any plausible compiler will produce broken code because the struct `s` is defined in a way that must have different sizes. The sentence `In most situations, the flexible array member is ignored.` in formal terms is meaningless or outright harmful. It doesn't even say 'unless otherwise stated, ignored' so leaves a bit to the imagination. – Persixty Aug 26 '22 at 09:26
  • @Fe2O3 In addition, to reply to a comment you made on a now-deleted answer, the ONLY conclusion that a newbie should remember of this question if they happened to read it is that the program shown is not [intended to be] defined by the C17 standard. This is what the question asks and what the accepted answer answers. The question does not ask whether it is a good idea to use FAMs and the answer does not cover what would be a different question. – Pascal Cuoq Aug 26 '22 at 12:05

1 Answers1

7

As noted in the question, C 2018 6.78.2.1 18 says:

… In most situations, the flexible array member is ignored…

We may regard this as ignoring the flexible array member except where otherwise stated or where necessity dictates it. (For the latter, I am considering alignment requirement. The standard explicitly says a structure with a flexible array member may have more trailing padding than it would without the flexible array member but omits mention of the fact that it may have a greater alignment requirement. But clearly a flexible array member may impose a greater alignment requirement, if its elements have a greater requirement than other members of the structure.)

Since, for purposes of determining compatibility, no exception of ignoring the flexible array member is stated, we should ignore the flexible array member for purposes of determining compatibility (but not ignore its extra padding and alignment requirement). Then, applying the rule in 6.7.2.1 1, we see that the two struct s declarations in the question do not have a “one-to-one correspondence between their members,” since one has an array member at the end and the other, when we ignore the flexible array member, does not.

Further, I would regard the lack of mention of the potential additional padding in 6.7.2.1 1 (and the lack of mention of additional alignment requirement) as evidence the committee failed to fully consider the effects of flexible array members on the statements of compatibility in 6.7.2.1 1, and 6.7.2.1 1 is therefore incomplete.

The above attempt to wrest meaning out of words written imperfectly by humans leaves open the possibility that a structure type with a flexible array member would be deemed compatible with a structure type without the flexible array member with the same alignment requirement (and hence the same trailing padding). That may be an unintentional consequence, but may not cause any problems—the two types are deemed compatible only when declared in separate translation units and will behave the same for assignment and other actions except that the flexible array member will be accessible in one translation unit and not in the other.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • Note also [C11 (draft)'s 6.7.2.1p18](http://port70.net/~nsz/c/c11/n1570.html#6.7.2.1p18): "If this array would have no elements, it behaves as if it had one element but the behavior is undefined if any attempt is made to access that element or to generate a pointer one past it." That seems to me to make the usage here explicitly undefined behavior, but I'll defer to your interpretation. – Andrew Henle Aug 26 '22 at 11:46
  • 1
    If they went with the more conventional language of "unless otherwise stated, ignored" it would all read so much better. "mostly ignored" is not the language of good specification. I'd say alignment of the type and padded as if the size had some positive integer value and size as if declared `T x[1]` minus `sizeof(T)`. We know that's the intent but I think the consensus in this thread is that it doesn't quite manage to say that or at best doesn't say it very well. – Persixty Aug 26 '22 at 12:19