Is it valid to use a zero-sized non-static array function parameter?

933 Views Asked by At

Is it valid, according to ISO C (any version), to specify a zero-sized array parameter?

The standard seems ambiguous. While it's clear that zero-sized arrays are invalid, array function parameters are special:

C23::6.7.6.3/6:

A declaration of a parameter as "array of type" shall be adjusted to "qualified pointer to type", where the type qualifiers (if any) are those specified within the [ and ] of the array type derivation. If the keyword static also appears within the [ and ] of the array type derivation, then for each call to the function, the value of the corresponding actual argument shall provide access to the first element of an array with at least as many elements as specified by the size expression.

As long as you don't use static, the size specified between [] is effectively ignored. As I understand the quoted paragraph, the compiler isn't allowed to make any suppositions at all about the pointer.

So, the following code should be conforming, right?

void h(char *start, char past_end[0]);

#define size 100
void j(void)
{
        char dst[size];
        h(dst, dst+size);
}

I use past_end[0] as a sentinel pointer to one-past-the-end (instead of a size; it's much more comfortable in some cases). The [0] clearly tells that it's one past the end, and not the actual end, which as a pointer, readers might confuse. The end would be marked as end[1], to be clear.

GCC thinks it's not conforming:

$ gcc -Wall -Wextra -Wpedantic -pedantic-errors -std=c17 -S ap.c 
ap.c:1:26: error: ISO C forbids zero-size array ‘past_end’ [-Wpedantic]
    1 | void h(char *start, char past_end[0]);
      |                          ^~~

Clang seems to agree:

$ clang -Wall -Wextra -Wpedantic -pedantic-errors -std=c17 -S ap.c 
ap.c:1:30: warning: zero size arrays are an extension [-Wzero-length-array]
void h(char *start, char past_end[0]);
                                  ^
1 warning generated.

If I don't ask for strict ISO C, GCC still warns (differently), while Clang relaxes:

$ cc -Wall -Wextra -S ap.c 
ap.c: In function ‘j’:
ap.c:7:9: warning: ‘h’ accessing 1 byte in a region of size 0 [-Wstringop-overflow=]
    7 |         h(dst, dst+size);
      |         ^~~~~~~~~~~~~~~~
ap.c:7:9: note: referencing argument 2 of type ‘char[0]’
ap.c:1:6: note: in a call to function ‘h’
    1 | void h(char *start, char past_end[0]);
      |      ^
ap.c:7:9: warning: ‘h’ accessing 1 byte in a region of size 0 [-Wstringop-overflow=]
    7 |         h(dst, dst+size);
      |         ^~~~~~~~~~~~~~~~
ap.c:7:9: note: referencing argument 2 of type ‘char[0]’
ap.c:1:6: note: in a call to function ‘h’
    1 | void h(char *start, char past_end[0]);
      |      ^
$ clang -Wall -Wextra -S ap.c 

I reported this to GCC, and there seems to be disagreement:

https://gcc.gnu.org/pipermail/gcc/2022-December/240277.html

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=108036

Is there any requirements to comply with the same requirements as for arrays?

Moreover, if that proves to be true, what about the following?:

void f(size_t sz, char arr[sz]);

Is the compiler entitled, under strict ISO C mode, to assume that the array will always have at least one element, even if I didn't use static, just because I used array syntax? If so, that would probably be a regression in the language.

3

There are 3 best solutions below

1
Eric Postpischil On BEST ANSWER

Is it valid, according to ISO C (any version), to specify a zero-sized array parameter?

The C standard is clear. C 2018 6.7.6.2 1, which is a constraints paragraph, says:

In addition to optional type qualifiers and the keyword static, the [ and ] may delimit an expression or *. If they delimit an expression (which specifies the size of an array), the expression shall have an integer type. If the expression is a constant expression, it shall have a value greater than zero…

Since it is a constraint, the compiler must issue a diagnostic message about it. However, as with most things in C, the compiler may accept it anyway.

The adjustment of an array parameter to a pointer is irrelevant; that must come later, since a declaration of a parameter to a pointer cannot be adjusted until there is a declaration to adjust. So the declaration has to be parsed, and it is subject to the constraints of that.

The [0] clearly tells that it's one past the end, and not the actual end, which as a pointer, readers might confuse.

You might use it to tell human readers that, but it does not mean that to the compiler. [static n] tells the compiler there are at least n elements, and [n] has even less meaning than that. It is valid to pass a subarray to a function—to pass a pointer into the middle of an array with the function intended to be used only to access a subset of the array reaching neither to the start or the end of the original array, so, even if [0] were accepted, it would not necessarily mean the pointer is pointing to the end of the array. It would be valid to point anywhere into an array.

11
dbush On

This is an interesting corner case.

Section 6.7.6.2p1 of the C11 standard specifying a constraint for array declarators states:

In addition to optional type qualifiers and the keyword static, the [ and ] may delimit an expression or *. If they delimit an expression (which specifies the size of an array), the expression shall have an integer type. If the expression is a constant expression, it shall have a value greater than zero. The element type shall not be an incomplete or function type. The optional type qualifiers and the keyword static shall appear only in a declaration of a function parameter with an array type, and then only in the outermost array type derivation.

What you've shown constitutes a constraint violation which requires a warning / error and is considered undefined behavior. But at the same time, because you have an array declared as a parameter to a function, the array gets adjusted to pointer type as per the passage you stated.

Strictly speaking, what the compiler is showing is correct, and in fact it's required to do so in order to be a strictly conforming implementation, however I'd have a hard time arguing that it makes sense for a compiler to reject a program that has char past_end[0] as a function parameter when it is equivalent to char *past_end.

2
supercat On

An important thing to understand about the C89 Standard (which is also relevant when looking at subsequent versions) is that it involved compromises between the compile writers who didn't want to change the behavior of existing compilers that would reject certain constructs, and programmers who used other compilers that would accept those constructs, who didn't want to have to change their code.

In many such situations, the compromise that was reached was that the Standard would impose a constraint that would require that conforming compilers issue a diagnostic, but compilers whose customers would regard the constraint as silly would be free to accept the code after the diagnostic was issued. If the programmers would be satisfied with their code being "conforming", rather than "strictly conforming", they could then proceed to ignore the constraint if both they and the authors of their compiler thought it was silly.

There's a reason that the flag which enables warnings about zero-sized arrays is named "-pedantic". The authors of gcc recognized that the language would be better off without the constraint, but they provided an option to output a diagnostic in case it was violated, so as to satisfy the constraints demanded by pedants.