Search code examples
clanguage-lawyerc99c11null-pointer

Inconsistent C99 support in gcc and clang


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);
      |     ^~~~~~~~

Solution

  • 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.