6

When trying to take advantage of the C99 function prototype syntax specifying non null pointers for function arguments, I came across some inconsistent behavior between clang and gcc:

A function can be declared and defined to receive a non-null pointer to an array of a minimum size. For example:

char *mystrcpy(char dest[restrict static 1], char const src[restrict static 1]);

declares function mystrcpy to take non-null restricted pointers to char arrays.

This definition is stricter than the standard definition of strcpy that uses a more classic form:

char *strcpy(char restrict *dest, const char restrict *src);

Where the C compiler is given no information about the constraint ont the arguments to be non-null.

I wrote a test program to verify the compatibility of these 2 prototypes and was surprised to discover that they are indeed compatible although more information is carried in the first than the second. Further surprising were these facts:

  • with all warnings enabled, clang does not complain about strcpy receiving null arguments.
  • gcc did complain about strcpy receiving null arguments, not not about mystrcpy, in spite of its unambiguous definition.
  • assigning strcpy or mystrcpy to function pointers defined with both syntaxes did not cause any warning.
  • passing null pointers to an indirect call through a function pointer did not trigger a warning in clang where the direct call did.

My question is: are these observations consistent with the C Standard or are gcc and/or clang incorrect in their implementation of C99's static keyword inside the [] of a function argument?

Here is the code:

#include <stdio.h>
#include <string.h>

static char *mystrcpy(char dest[restrict static 1], char const src[restrict static 1]) {
    char *p = dest;
    while ((*p++ = *src++) != '\0')
        continue;
    return dest;
}

static char *(*f1)(char *dest, const char *src) = strcpy;
static char *(*f2)(char *dest, const char *src) = mystrcpy;

static char *(*f3)(char dest[restrict static 1], char const src[restrict static 1]) = strcpy;
static char *(*f4)(char dest[restrict static 1], char const src[restrict static 1]) = mystrcpy;

int main() {
    char a[100];

    strcpy(a, "a");
    strcpy(a, "");
    strcpy(a, NULL);
    strcpy(a, a);
    strcpy(NULL, a);
    strcpy(NULL, NULL);

    mystrcpy(a, "a");
    mystrcpy(a, "");
    mystrcpy(a, NULL);
    mystrcpy(a, a);
    mystrcpy(NULL, a);
    mystrcpy(NULL, NULL);

    f1(a, "a");
    f1(a, "");
    f1(a, NULL);
    f1(a, a);
    f1(NULL, a);
    f1(NULL, NULL);

    f2(a, "a");
    f2(a, "");
    f2(a, NULL);
    f2(a, a);
    f2(NULL, a);
    f2(NULL, NULL);

    f3(a, "a");
    f3(a, "");
    f3(a, NULL);
    f3(a, a);
    f3(NULL, a);
    f3(NULL, NULL);

    f4(a, "a");
    f4(a, "");
    f4(a, NULL);
    f4(a, a);
    f4(NULL, a);
    f4(NULL, NULL);

    return 0;
}

gcc output: it only complains about the direct calls to strcpy with NULL arguments.

$ gcc -O2 -std=c99 -Wall -Wextra -W -o sc sc.c
sc.c: In function 'main':
sc.c:22:5: warning: null argument where non-null required (argument 2) [-Wnonnull]
sc.c:22:5: warning: null argument where non-null required (argument 2) [-Wnonnull]
sc.c:24:5: warning: null argument where non-null required (argument 1) [-Wnonnull]
sc.c:24:5: warning: null argument where non-null required (argument 1) [-Wnonnull]
sc.c:25:5: warning: null argument where non-null required (argument 1) [-Wnonnull]
sc.c:25:5: warning: null argument where non-null required (argument 2) [-Wnonnull]
sc.c:25:5: warning: null argument where non-null required (argument 1) [-Wnonnull]
sc.c:25:5: warning: null argument where non-null required (argument 2) [-Wnonnull]

clang's output: only complains about the direct calls to mystrcpy with NULL arguments.

$ clang -Weverything -o sc sc.c
sc.c:29:5: warning: null passed to a callee that requires a non-null argument [-Wnonnull]
    mystrcpy(a, NULL);
    ^           ~~~~
sc.c:4:64: note: callee declares array parameter as static here
static char *mystrcpy(char dest[restrict static 1], char const src[restrict static 1]) {
                                                               ^  ~~~~~~~~~~~~~~~~~~~
sc.c:31:5: warning: null passed to a callee that requires a non-null argument [-Wnonnull]
    mystrcpy(NULL, a);
    ^        ~~~~
sc.c:4:28: note: callee declares array parameter as static here
static char *mystrcpy(char dest[restrict static 1], char const src[restrict static 1]) {
                           ^   ~~~~~~~~~~~~~~~~~~~
sc.c:32:5: warning: null passed to a callee that requires a non-null argument [-Wnonnull]
    mystrcpy(NULL, NULL);
    ^        ~~~~
sc.c:4:28: note: callee declares array parameter as static here
static char *mystrcpy(char dest[restrict static 1], char const src[restrict static 1]) {
                           ^   ~~~~~~~~~~~~~~~~~~~
sc.c:32:5: warning: null passed to a callee that requires a non-null argument [-Wnonnull]
    mystrcpy(NULL, NULL);
    ^              ~~~~
sc.c:4:64: note: callee declares array parameter as static here
static char *mystrcpy(char dest[restrict static 1], char const src[restrict static 1]) {
                                                               ^  ~~~~~~~~~~~~~~~~~~~
4 warnings generated.

A more recent version of gcc also complains about passing the same pointer for 2 restrict qualified arguments, but still no support for the static minimum length specifier (see Godbolt session):

sc.c:30:14: warning: passing argument 1 to 'restrict'-qualified parameter aliases with argument 2 [-Wrestrict]
   30 |     mystrcpy(a, a);
      |              ^  ~
sc.c:51:8: warning: passing argument 1 to 'restrict'-qualified parameter aliases with argument 2 [-Wrestrict]
   51 |     f3(a, a);
      |        ^  ~
sc.c:58:8: warning: passing argument 1 to 'restrict'-qualified parameter aliases with argument 2 [-Wrestrict]
   58 |     f4(a, a);
      |        ^  ~
sc.c:37:5: warning: 'strcpy' source argument is the same as destination [-Wrestrict]
   37 |     f1(a, a);
      |     ^~~~~~~~
chqrlie
  • 131,814
  • 10
  • 121
  • 189
  • 2
    The purpose of `restrict` is permitting compiler optimisations, not forcing errors for violated constraints. If the programmer writes code that doesn't comply with aliasing assertions made using `restrict` - such as by passing a NULL pointer to your `mystrcpy()` - then the behaviour is undefined. That means no diagnostic is required. In other words, the behaviour you are expecting is not required at all by the standard. Both compilers are therefore correct - in the sense that undefined behaviour means they are permitted to do anything, – Peter Aug 14 '19 at 12:01
  • I don't know about clang, but I'm pretty sure gcc just ignores the minimum array length qualifiers. You're using an old version though and I haven't tested lately with current releases. – Shawn Aug 14 '19 at 12:04
  • @Peter: I am well aware of the `restrict` semantics, which by the way are almost meaningless in a function declaration. I only put the `restrict` keywords for compatibility with the definition of `strcpy`, but the problem I am documenting relates to the `char p[static 1]` syntax which specifies that the function argument `p` should be non-null and point to an array of at least 1 char. – chqrlie Aug 14 '19 at 12:06
  • You might be interested in playing with gcc's `nonnull` [function attribute](https://gcc.gnu.org/onlinedocs/gcc-9.2.0/gcc/Common-Function-Attributes.html) btw. – Shawn Aug 14 '19 at 12:09
  • 1
    @Shawn: gcc's `nonnull` attribute is what is causing the wanings for `strcpy`. I want to use a standard feature with the same semantics and I'm afraid it is not supported by gcc, unlike clang. – chqrlie Aug 14 '19 at 12:10
  • 1
    I think this might answer your question: https://stackoverflow.com/a/3430353/5218277 – Alex Lop. Aug 14 '19 at 12:21
  • 1
    Same comment goes for your use of `static` - for example, see https://stackoverflow.com/questions/3430315/what-is-the-purpose-of-static-keyword-in-array-parameter-of-function-like-char – Peter Aug 14 '19 at 12:25

4 Answers4

4

Based on this answer, the compiler doesn't have to diagnose the call to such function and verify that the parameters meet the static qualifier.

Note that the C Standard does not require the compiler to diagnose when a call to the function does not meet these requirements (i.e. it is silent undefined behaviour).


EDIT:

Based on the Standard C99:

6.7.5.3 Function declarators (including prototypes)

...

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.

...

J.2 Undefined behavior

...

— A declaration of an array parameter includes the keyword static within the [ and ] and the corresponding argument does not provide access to the first element of an array with at least the specified number of elements (6.7.5.3).

Alex Lop.
  • 6,810
  • 1
  • 26
  • 45
  • Correct answer. I'd just like to add that the requirement for the pointer not being null cannot be a constraint violation, because there are situations where the compiler can't know. – Jens Gustedt Aug 14 '19 at 20:16
  • @JensGustedt, right but that's true for many other things. For example `int x; ... x << 32` the compiler will fire a warning but here `x << y` the compiler just can't (always) know the value of `y` at compilation time. – Alex Lop. Aug 15 '19 at 05:40
  • @JensGustedt: Further, a compiler can't generally tell whether a particular function will ever be called, and the existence of that a function could never have defined behavior would only be allowed to affect program behavior in cases where it's not called if it would cause a program to exceed a translation limit. A conforming compiler could impose a "translation limit" of zero functions that could not be executed while invoking UB, but that would seem an abuse of "translation limit" allowances. – supercat Aug 15 '19 at 17:06
1

GCC just ignores static and the arguments of strcpy is declared non-null with __attribute__((nonnull)), which is actually more useful than static 1: you cannot use the latter for a function that has void * argument because the array syntax in arguments requires that the type of element is a completed type, neither can you use it for a pointer to an array of undeclared size, or to an opaque struct.

As for compatibility, C11 6.7.6.3p21 says:

21 EXAMPLE 5 The following are all compatible function prototype declarators.

[....]

       void   f(double      (* restrict a)[5]);
       void   f(double      a[restrict][5]);
       void   f(double      a[restrict 3][5]);
       void   f(double      a[restrict static 3][5]);

Finally, the behaviour of the static is defined under a semantics section, not in constraints, so it needs not be diagnosed. It could be but it doesn't seem to have been implemented in GCC. Here's one bug report.

  • Good point! neither is there a syntax to attach the nonnull semantics not the minimum size to individual pointers, such as the return value of `strcpy`... making the consistency check across function calls mostly impossible. – chqrlie Aug 14 '19 at 12:37
1

My question is: are these observations consistent with the C Standard or are gcc and/or clang incorrect in their implementation of C99's static keyword inside the [] of a function argument?

The observations are consistent with the standard.

With respect to diagnostics, the standard provides that

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.

, but that is part of the semantic description, not a constraint, so there is no requirement that implementations produce diagnostics about violations. Code that violates that provision has undefined behavior, of course, but that's a separate matter.

And even if there were a constraint violation, a conforming implementation is not obligated to reject the code; the only requirement on the implementation in such a case is that it emit a diagnostic message.

As for function-pointer type compatibility, the standard specifies that

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.

static is not among the type qualifiers (they are zero or more of const, restrict, and volatile), so its appearance in the function signature does not serve to alter the function's type. Thus, pointers to these two functions

char *mystrcpy(char dest[restrict static 1], char const src[restrict static 1]);

[...]

char *strcpy(char restrict *dest, const char restrict *src);

indeed do have compatible (in fact the same) type. The static 1 simply does not factor in to that.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
0

From the answer to question What is the purpose of static keyword in array parameter of function like "char s[static 10]"? it appears to be a quality of implementation issue. The C Standard does not specify that the compiler has to diagnose such obvious cases of undefined behavior.

As a matter of fact, the prototypes for functions mystrcpy and strcpy are compatible as documented in an example at the end C18 6.7.6.3 Function declarators (including prototypes), removing much of the advantage of the added information.

It seems much of the new features added in C99 have never caught on, as they were not implemented in some of the mainstream compilers, for political and technical reasons, not the least of which was the reluctance to introduce new incompatibilities with C++.

As a consequence, this ill-born feature, with a horrible syntax, is quite useless.

chqrlie
  • 131,814
  • 10
  • 121
  • 189