gcc 和 clang 中的 C99 支持不一致

Inconsistent C99 support in gcc and clang

在尝试利用为函数参数指定非空指针的 C99 函数原型语法时,我遇到了 clang 和 gcc 之间的一些不一致行为:

可以声明和定义函数以接收指向最小大小数组的非空指针。例如:

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

声明函数 mystrcpy 以获取指向 char 数组的非空受限指针。

此定义比 strcpy 的标准定义更严格,后者使用更经典的形式:

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

C 编译器没有给出关于参数为非空的约束的信息。

我编写了一个测试程序来验证这两个原型的兼容性,并惊讶地发现它们确实兼容,尽管第一个比第二个携带的信息更多。更令人惊讶的是这些事实:

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?

代码如下:

#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++) != '[=10=]')
        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 输出:它仅抱怨使用 NULL 个参数直接调用 strcpy

$ 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 的输出:仅抱怨使用 NULL 个参数直接调用 mystrcpy

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

较新版本的 gcc 也抱怨为 2 个 restrict 限定参数传递相同的指针,但仍然不支持 static 最小长度说明符(参见 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);
      |     ^~~~~~~~

基于this answer,编译器不必诊断对此类函数的调用并验证参数是否满足static 限定符。

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


编辑:

基于标准 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).

GCC 只是忽略 static 并且 strcpy 的参数用 __attribute__((nonnull)) 声明为非空,这实际上比 static 1 更有用:你不能使用后者用于具有 void * 参数的函数,因为参数中的数组语法要求元素类型为 completed 类型,您也不能将其用作指向数组的指针未声明的大小,或不透明 struct.

至于兼容性,C11 6.7.6.3p21 说:

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]);

最后,static 的行为是 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

从问题 What is the purpose of static keyword in array parameter of function like "char s[static 10]"? 的答案来看,这似乎是实施质量问题。 C 标准没有指定编译器必须诊断这种明显的未定义行为情况。

事实上,函数 mystrcpystrcpy 的原型是兼容的,如末尾示例中所述 C18 6.7.6.3 函数声明符(包括原型),删除了添加信息的大部分优势。

由于政治和技术原因,C99 中添加的许多新功能似乎从未流行起来,因为它们没有在一些主流编译器中实现,其中最重要的是不愿引入新的不兼容性使用 C++。

因此,这个语法糟糕的生来就有病的功能毫无用处。

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?

观察结果符合标准

关于诊断,标准规定

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.

,但这是 语义 描述的一部分,而不是约束,因此不要求实现产生有关违规的诊断。当然,违反该规定的代码具有未定义的行为,但这是另一回事。

并且即使存在约束违规,符合规范的实现也没有义务拒绝代码;在这种情况下,对实现的唯一要求是发出诊断消息。

至于函数指针类型兼容性,标准规定

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 不属于类型限定符(它们是 constrestrictvolatile 中的零个或多个),因此它出现在函数签名中不用于改变函数的类型。因此,指向这两个函数的指针

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

[...]

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

确实有兼容的(实际上是相同的)类型。 static 1 根本没有考虑到这一点。