使用零大小的非静态数组函数参数是否有效?

Is it valid to use a zero-sized non-static array function parameter?

提问人:alx - recommends codidact 提问时间:12/13/2022 更新时间:12/16/2022 访问量:928

问:

根据 ISO C(任何版本),指定零大小的数组参数是否有效?

这个标准似乎模棱两可。虽然很明显零大小的数组是无效的,但数组函数参数很特殊:

C23::6.7.6.3/6:

将参数声明为“类型数组”应调整为 “限定的指向类型的指针”,其中类型限定符(如果有)是 在数组类型派生的 [ 和 ] 中指定的那些。如果 关键字 static 也出现在数组类型的 [ 和 ] 中 推导,则对于对函数的每次调用,值 相应的实际参数应提供对第一个 元素,其元素数量至少与 size 表达式。

只要不使用 ,指定的 size 就会被有效地忽略。据我了解引用的段落,编译器根本不允许对指针做出任何假设。static[]

所以,下面的代码应该是符合的,对吧?

void h(char *start, char past_end[0]);

#define size 100
void j(void)
{
        char dst[size];
        h(dst, dst+size);
}

我用作指向 one-past-the-end 的哨兵指针(而不是大小;在某些情况下它更舒适)。清楚地告诉人们,这是一个过去的结束,而不是实际的结束,作为一个指针,读者可能会感到困惑。为了清楚起见,结尾将标记为 。past_end[0][0]end[1]

GCC认为它不符合:

$ gcc -Wall -Wextra -Wpedantic -pedantic-errors -std=c17 -S ap.c 
ap.c:1:26: error: ISO C forbids zero-size array ‘past_end’ [-Wpedantic]
    1 | void h(char *start, char past_end[0]);
      |                          ^~~

Clang似乎同意:

$ clang -Wall -Wextra -Wpedantic -pedantic-errors -std=c17 -S ap.c 
ap.c:1:30: warning: zero size arrays are an extension [-Wzero-length-array]
void h(char *start, char past_end[0]);
                                  ^
1 warning generated.

如果我不要求严格的 ISO C,GCC 仍然会发出警告(不同),而 Clang 会放松:

$ cc -Wall -Wextra -S ap.c 
ap.c: In function ‘j’:
ap.c:7:9: warning: ‘h’ accessing 1 byte in a region of size 0 [-Wstringop-overflow=]
    7 |         h(dst, dst+size);
      |         ^~~~~~~~~~~~~~~~
ap.c:7:9: note: referencing argument 2 of type ‘char[0]’
ap.c:1:6: note: in a call to function ‘h’
    1 | void h(char *start, char past_end[0]);
      |      ^
ap.c:7:9: warning: ‘h’ accessing 1 byte in a region of size 0 [-Wstringop-overflow=]
    7 |         h(dst, dst+size);
      |         ^~~~~~~~~~~~~~~~
ap.c:7:9: note: referencing argument 2 of type ‘char[0]’
ap.c:1:6: note: in a call to function ‘h’
    1 | void h(char *start, char past_end[0]);
      |      ^
$ clang -Wall -Wextra -S ap.c 

我向 GCC 报告了此事,但似乎存在分歧:

https://gcc.gnu.org/pipermail/gcc/2022-December/240277.html

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=108036

是否有任何要求来遵守与阵列相同的要求?

此外,如果这被证明是真的,那么以下情况呢?

void f(size_t sz, char arr[sz]);

在严格的 ISO C 模式下,编译器是否有权假设数组始终至少有一个元素,即使我没有使用 ,只是因为我使用了数组语法?如果是这样,那可能是语言的倒退。static

数组 C 指针 language-lawyer 函数-参数

评论

5赞 Lundin 12/13/2022
“[0] 清楚地表明它已经过了尽头”为什么?这是很清楚的,根本不是自我记录的。相反,处理从 到 的迭代的“事实上的标准”方法是命名项目,然后将 1 个项目指向数组之外。同样,族的点数超出有效字符串的 1 个项目,C++ 用于指向数组之外的 1 个项目,依此类推。startendendendptrstrtoliteratorend
4赞 Lundin 12/13/2022
“开始/结束”循环的惯用形式是:.这需要指向数组之外的 1 个项目。这就是 AFAIK 为什么 C 允许我们在数组之外指向 1 个项目,只要我们不取消引用该位置。for(const type* i = start; i != end; i++)end
0赞 Marco 12/14/2022
I use past_end[0] as a sentinel pointer to one-past-the-end (instead of a size; it's much more comfortable in some cases).什么?这让我感到困惑。将具有大小的数组传递给函数是非常习惯的。
0赞 alx - recommends codidact 12/16/2022
@marco-a 链接字符串复制函数,这些函数会截断,同时将截断检测推迟到所有链接调用之后,只能使用指针来完成(好吧,你可以用大小来做,但我敢写它并且是可读的并且不容易出错)。请参见:<software.codidact.com/posts/285946/287522#answer-287522>
0赞 alx - recommends codidact 12/16/2022
@Lundin 是的,在许多情况下,事实上的标准是用来引用最后一个有效指针。但是,我也看到很多用于引用最后一个字节的代码。这种不一致对我的口味来说太不一致了,事实上,我在代码库中发现了错误,其中给定的函数被有意义地实现,而在调用站点上,它被传递到实际的末尾;相差一个,你可以猜到。我想使用明确的语法来修复这种不一致的错误来源。endendendpast_end

答:

17赞 Eric Postpischil 12/13/2022 #1

根据 ISO C(任何版本),指定零大小的数组参数是否有效?

C标准很明确。C 2018 6.7.6.2 1,这是一个约束段落,说:

除了可选的类型限定符和关键字之外,and 还可以分隔表达式或 。如果它们分隔表达式(指定数组的大小),则表达式应具有整数类型。如果表达式是常量表达式,则其值应大于零...static[]*

由于它是一个约束,因此编译器必须发出有关它的诊断消息。但是,与 C 中的大多数内容一样,编译器无论如何都可能接受它。

数组参数对指针的调整无关紧要;这必须稍后进行,因为在有要调整的声明之前,无法调整指向指针的参数声明。因此,必须对声明进行解析,并且它受制于该声明的约束。

清楚地告诉人们,这是一个过去的结束,而不是实际的结束,作为一个指针,读者可能会感到困惑。[0]

你可以用它来告诉人类读者,但它对编译器来说并不意味着这一点。 告诉编译器至少有元素,甚至比这更没有意义。将子数组传递给函数是有效的,即将指针传递到数组的中间,该函数仅用于访问数组的子集,该子集既不到达原始数组的开头,也不到达原始数组的末尾,因此,即使被接受,也不一定意味着指针指向数组的末尾。指向数组中的任何位置都是有效的。[static n]n[n][0]

评论

0赞 alx - recommends codidact 12/13/2022
我知道被编译器忽略了。正如你所猜到的,我将其用于人类读者。:)[0]
6赞 dbush 12/13/2022 #2

这是一个有趣的极端情况。

C11 标准的第 6.7.6.2p1 节指定了数组声明符的约束,指出:

除了可选的类型限定符和关键字之外,and 还可以分隔表达式或 。如果它们分隔表达式 (指定数组的大小),表达式应具有 整数类型。如果表达式是常量表达式,则应 值大于零。元素类型不得为 不完整或函数类型。可选类型限定符和 关键字只能出现在函数的声明中 参数,然后仅在最外层的数组中 类型派生。static[]*static

您显示的内容构成约束冲突,需要警告/错误,并被视为未定义的行为。但与此同时,由于您有一个数组声明为函数的参数,因此该数组会根据您所说的段落调整为指针类型。

严格来说,编译器显示的内容是正确的,实际上,为了成为严格合规的实现,必须这样做,但是我很难说编译器拒绝具有作为函数参数的程序是有意义的,当它等价于 .char past_end[0]char *past_end

评论

0赞 Lundin 12/13/2022
我认为在一些语言律师的讨论中,已经确定参数列表中的数组必须是有效的数组,然后才能进行“数组调整”规则。例如,由于这是一个不完整元素类型的数组而被拒绝,您在此处引用的同一部分(数组声明符)也适用于该方案。“但是,我很难争辩说编译器拒绝这样的程序是有道理的” 与其他示例一样,它将拒绝 .否则它将如何对衰变类型执行指针算术?int x[][]int x[][0]int (*)[0]
0赞 dbush 12/13/2022
@Lundin 对于示例,是的,它无效是有道理的,因为没有调整零大小的数组部分。调整到这种情况似乎值得怀疑。int x[][0]int x[0]int *
1赞 Lundin 12/13/2022
为了理解它,编译器必须首先确定是否存在有效的声明。描述参数调整的文本甚至说 (6.7.5.3) “将参数声明为'类型数组''应调整为'限定的指向类型的指针''”。这意味着必须首先检查声明部分。
0赞 supercat 12/14/2022
@Lundin:在编译器不必知道或关心数组类型大小的上下文中,通过要求指定大小或虚拟占位符,甚至要求编译器识别虚拟占位符的特殊语法,会得到什么?
1赞 supercat 12/15/2022
@Lundin:鉴于 C 允许指向不完整类型的指针,我不认为零大小的类型会带来任何问题。为了有效,指针必须具有非零类型,并且只有在大小为非零的情况下才能有效地执行计算,但是如果数组有时需要,有时不需要(例如),能够将数组声明折叠为零大小,这比要求程序员跳过箍来实现这种效果更有用。此外,在以下情况下...ptr1-ptr2ptr1+integerptr1char padding[sizeof (blob) - 64];
2赞 supercat 12/14/2022 #3

关于 C89 标准(在查看后续版本时也很重要)需要了解的一件重要事情是,它涉及编译编写者之间的妥协,前者不想改变现有编译器的行为,从而拒绝某些构造,而后者使用其他编译器接受这些构造的程序员,他们不想更改他们的代码。

在许多此类情况下,达成的折衷方案是,标准将施加一个约束,要求符合要求的编译器发布诊断,但其客户认为该约束是愚蠢的编译器可以在发布诊断后自由接受代码。如果程序员对他们的代码是“符合”而不是“严格符合”感到满意,那么如果他们和编译器的作者都认为这是愚蠢的,他们就可以继续忽略约束。

启用有关零大小数组的警告的标志被命名为“-pedantic”是有原因的。gcc 的作者认识到,没有约束的语言会更好,但他们提供了一个选项,可以在违反时输出诊断,以满足学究所要求的约束。

评论

0赞 alx - recommends codidact 12/16/2022
但是,GCC 仍然以非迂腐模式发出警告(它认为可能存在缓冲区溢出)。当我建议这是 GCC 中的一个错误时,有不同的意见。一些维护者承认了这个错误,而其他人(包括 WG14 的一名成员)则表示,预计不会使用 0 大小的数组,编译器有权发出警告。我们将看到错误报告是如何演变的......
1赞 supercat 12/16/2022
@alx:委员会通常不会试图预测一个结构可能在不可移植程序中使用的目的,并且编译器被给予一揽子许可,可以警告他们认为合适的任何事情。虽然我认为长期以来一直需要一个文档来描述通用语言扩展,并提供一种区分支持它们的实现和不支持它们的实现的方法,但该标准从未寻求成为这样的文档。