【问题标题】:Inconsistent C99 support in gcc and clanggcc 和 clang 中的 C99 支持不一致
【发布时间】:2019-12-20 23:29:07
【问题描述】:

在尝试利用 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 编译器提供关于非空参数的约束的信息。

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

  • 启用所有警告后,clang 不会抱怨 strcpy 接收空参数。
  • gcc 确实抱怨 strcpy 接收空参数,而不是抱怨 mystrcpy,尽管它的定义明确。
  • strcpymystrcpy 分配给使用这两种语法定义的函数指针不会导致任何警告。
  • 通过函数指针将空指针传递给间接调用并不会在 clang 中触发直接调用的警告。

我的问题是:这些观察结果是否符合 C 标准,或者 gcc 和/或 clang 在函数参数的 [] 内实现 C99 的 static 关键字时是否不正确?

代码如下:

#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 输出:它只抱怨使用NULL 参数直接调用strcpy

$ gcc -O2 -std=c99 -Wall -Wextra -W -o sc sc.c sc.c:在函数'main'中: sc.c:22:5:警告:需要非空的空参数(参数 2)[-Wnonnull] sc.c:22:5:警告:需要非空的空参数(参数 2)[-Wnonnull] sc.c:24:5:警告:需要非空的空参数(参数 1)[-Wnonnull] sc.c:24:5:警告:需要非空的空参数(参数 1)[-Wnonnull] sc.c:25:5:警告:需要非空的空参数(参数 1)[-Wnonnull] sc.c:25:5:警告:需要非空的空参数(参数 2)[-Wnonnull] sc.c:25:5:警告:需要非空的空参数(参数 1)[-Wnonnull] sc.c:25:5:警告:需要非空的空参数(参数 2)[-Wnonnull]

clang 的输出:只抱怨使用 NULL 参数直接调用 mystrcpy

$ clang -Weverything -o sc sc.c sc.c:29:5:警告:null 传递给需要非 null 参数的被调用者 [-Wnonnull] mystrcpy(a, NULL); ^ ~~~~ sc.c:4:64:注意:被调用者在此处将数组参数声明为静态 static char *mystrcpy(char dest[restrict static 1], char const src[restrict static 1]) { ^ ~~~~~~~~~~~~~~~~~~~ sc.c:31:5:警告:null 传递给需要非 null 参数的被调用者 [-Wnonnull] mystrcpy(NULL, a); ^ ~~~~ sc.c:4:28:注意:被调用者在此处将数组参数声明为静态 static char *mystrcpy(char dest[restrict static 1], char const src[restrict static 1]) { ^ ~~~~~~~~~~~~~~~~~~~ sc.c:32:5:警告:null 传递给需要非 null 参数的被调用者 [-Wnonnull] mystrcpy(NULL, NULL); ^ ~~~~ sc.c:4:28:注意:被调用者在此处将数组参数声明为静态 static char *mystrcpy(char dest[restrict static 1], char const src[restrict static 1]) { ^ ~~~~~~~~~~~~~~~~~~~ sc.c:32:5:警告:null 传递给需要非 null 参数的被调用者 [-Wnonnull] mystrcpy(NULL, NULL); ^ ~~~~ sc.c:4:64:注意:被调用者在此处将数组参数声明为静态 static char *mystrcpy(char dest[restrict static 1], char const src[restrict static 1]) { ^ ~~~~~~~~~~~~~~~~~~~ 生成 4 个警告。

较新版本的 gcc 也抱怨为 2 个限制限定参数传递相同的指针,但仍然不支持 static 最小长度说明符(请参阅 Godbolt session):

sc.c:30:14:警告:将参数 1 传递给带有参数 2 [-Wrestrict] 的“restrict”限定参数别名 30 | mystrcpy(a, a); | ^ ~ sc.c:51:8:警告:将参数 1 传递给带有参数 2 [-Wrestrict] 的“restrict”限定参数别名 51 | f3(一,一); | ^ ~ sc.c:58:8:警告:将参数 1 传递给带有参数 2 [-Wrestrict] 的“restrict”限定参数别名 58 | f4(一,一); | ^ ~ sc.c:37:5:警告:“strcpy”源参数与目标 [-Wrestrict] 相同 37 | f1(一,一); | ^~~~~~~~

【问题讨论】:

  • restrict 的目的是允许编译器优化,而不是强制违反约束的错误。如果程序员编写的代码不符合使用 restrict 的别名断言 - 例如通过将 NULL 指针传递给您的 mystrcpy() - 那么行为是未定义的。这意味着不需要诊断。换句话说,标准根本不需要您期望的行为。因此,两个编译器都是正确的——从某种意义上说,未定义的行为意味着它们被允许做任何事情,
  • 我不知道 clang,但我很确定 gcc 只是忽略了最小数组长度限定符。不过,您使用的是旧版本,而我最近没有对当前版本进行测试。
  • @Shawn: gcc 的nonnull 属性是导致strcpy 衰减的原因。我想使用具有相同语义的标准功能,但与 clang 不同,恐怕 gcc 不支持它。
  • 我想这可能会回答你的问题:stackoverflow.com/a/3430353/5218277
  • static 的使用也有相同的评论 - 例如,请参阅stackoverflow.com/questions/3430315/…

标签: c language-lawyer c99 c11 null-pointer


【解决方案1】:

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

请注意,C 标准不要求编译器诊断 当对函数的调用不满足这些要求时(即 是无声的未定义行为)。


编辑:

基于标准 C99:

6.7.5.3 函数声明符(包括原型)

...

将参数声明为“类型数组”应调整为 ''qualified pointer to type'',其中类型限定符(如果有)是 在数组类型派生的 [ 和 ] 中指定的那些。如果 关键字 static 也出现在数组类型的 [ 和 ] 中 推导,然后对于函数的每次调用, 相应的实际参数应提供对第一个的访问 数组的元素至少与指定的元素一样多 大小表达式。

...

J.2 未定义行为

...

——数组参数的声明包含关键字static 在 [ 和 ] 中,并且相应的参数不提供 访问具有至少指定的数组的第一个元素 元素数量 (6.7.5.3)。

【讨论】:

  • 正确答案。我想补充一点,指针不为空的要求不能违反约束,因为有些情况编译器无法知道。
  • @JensGustedt,是的,但对于许多其他事情都是如此。例如int x; ... x &lt;&lt; 32 编译器会发出警告,但这里x &lt;&lt; y 编译器在编译时无法(总是)知道y 的值。
  • @JensGustedt:此外,编译器通常无法判断是否会调用特定函数,并且该函数的存在永远不会定义行为只会被允许影响程序行为如果它会导致程序超出翻译限制,则不会调用它的情况。符合标准的编译器可以对调用 UB 时无法执行的零个函数施加“翻译限制”,但这似乎是对“翻译限制”许可的滥用。
【解决方案2】:

我的问题是:这些观察结果是否符合 C 标准 或者 gcc 和/或 clang 在 C99 的实现中不正确 函数参数的[] 内的static 关键字?

观察结果符合标准。

关于诊断,该标准规定

如果关键字static 也出现在[] 的 数组类型推导,然后对于函数的每次调用,值 对应的实际参数应提供对第一个 数组的元素至少与指定的元素一样多 大小表达式。

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

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

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

将参数声明为“类型数组”应调整为 ''限定类型的指针'',其中类型限定符(如果有)是 在数组类型派生的 [ 和 ] 中指定的那些。

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 根本不考虑这一点。

【讨论】:

    【解决方案3】:

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

    至于兼容性,C11 6.7.6.3p21 说:

    21 示例 5 以下都是兼容的函数原型声明器。

    [....]

           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 的行为在 semantics 部分下是 defined,不受约束,因此不需要诊断。可能是,但它似乎没有在 GCC 中实现。 Here's one bug report.

    【讨论】:

    • 好点!也没有语法将非空语义而不是最小大小附加到单个指针,例如strcpy的返回值...使得跨函数调用的一致性检查几乎不可能。
    【解决方案4】:

    从问题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++ 的新不兼容性.

    因此,这种不合格的特性,加上可怕的语法,是毫无用处的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-04-11
      • 2016-12-10
      • 1970-01-01
      • 2021-05-08
      • 1970-01-01
      • 1970-01-01
      • 2017-02-09
      • 2014-06-22
      相关资源
      最近更新 更多