Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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);
      |     ^~~~~~~~
like image 726
chqrlie Avatar asked Aug 14 '19 11:08

chqrlie


2 Answers

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

like image 68
Alex Lop. Avatar answered Oct 06 '22 10:10

Alex Lop.


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.

like image 30
John Bollinger Avatar answered Oct 06 '22 10:10

John Bollinger