为啥零长度 VLA 是 UB?

Posted

技术标签:

【中文标题】为啥零长度 VLA 是 UB?【英文标题】:Why Are Zero Length VLAs UB?为什么零长度 VLA 是 UB? 【发布时间】:2016-01-25 22:51:15 【问题描述】:

2011 年标准明确规定...

6.7.6.2 数组声明符

    如果大小是一个不是整数常量表达式的表达式:如果它出现在一个 在函数原型范围内声明,它被视为被替换为 *;否则, 每次对其进行评估时,它的值都应大于零。每个实例的大小 可变长度数组类型在其生命周期内不会改变。如果大小表达式是 sizeof 运算符的操作数的一部分,并且更改大小表达式的值不会影响运算符的结果,则未指定是否 计算大小表达式。

这是人为的,但下面的代码似乎是合理的。

size_t vla(const size_t x) 

  size_t a[x];
  size_t y = 0;

  for (size_t i = 0; i < x; i++)
    a[x] = i;

  for (size_t i = 0; i < x; i++)
    y += a[i % 2];

  return y;

Clang 似乎为它生成了合理的 x64 程序集(没有优化)。显然索引一个零长度的 VLA 没有意义,但是越界访问会调用未定义的行为。

为什么零长度数组未定义?

【问题讨论】:

C 也不允许零长度的非 VLA;不允许它们作为 VLA 是一致的。 GCC(因此也有 clang)具有允许零长度数组的扩展。你可以争论这是否好。 “显然索引零长度 VLA 没有意义,但越界访问会调用未定义的行为。” - 示例中都没有发生。 @KarolyHorvath 我的想法是索引零长度的东西已经被禁止了。与空列表或零长度向量类似,只要值没有被索引(语言已经禁止),零长度数组对我来说是有意义的。 @JonathanLeffler 有趣的是,std::array in C++ does special case 的长度为零。 @Jason,该语言不禁止索引零长度数组-语法允许!只有越界访问的结果是UB。这适用于所有数组,与类型或大小无关。 【参考方案1】:
int i = 0;
int a[i], b[i];

a == b?它不应该——它们是不同的对象——但避免它是有问题的。如果您无条件地在ab 之间留下一个空白,那么您在i &gt; 0 案例中浪费了空间。如果您检查是否i == 0 并且只留下一个空白,那么您在i &gt; 0 案例中浪费时间。

多维数组会变得更糟:

int i = 0;
int a[2][i];

你可以在两个变量之间填充,但是你可以在哪里填充呢?如果不破坏sizeof (int[2][i]) == 2 * i * sizeof (int) 的不变量,就没有办法做到这一点。如果您不填充,那么a[0]a[1] 具有相同的地址,并且您正在破坏不同的重要不变量。

这是一个不值得定义的令人头疼的问题。

【讨论】:

"如果是肯定的,我会为 a 和 b 分配相同的地址" - 这是脑放屁还是一些错字?这没有任何意义...... @KarolyHorvath: "for positive i" 附加到它前面的短语,而不是后面的短语。生成的代码(对于积极的i 来说是合理且节省空间的)将为ab 分配相同的地址为i == 0 刮掉整个文本并从零开始。它仍然是一个巨大的混乱。 @KarolyHorvath:对我来说似乎并不难解析,但我写了它。现在怎么样? 我喜欢我们的回答,但是“如果你在 a 和 b 之间无条件地留出一个间隙,那么你在 i > 0 的情况下是在浪费空间”,如果“无条件”意味着“至少一个元素”(如果你明白我的意思的话)。如果 i>0 则没有浪费。【参考方案2】:

虽然我们可以看到gcc supports zero length arrays an extension,但很明显它们很有用。从标准的角度来看,这似乎会产生一些问题,因为现在每个对象都应该有一个唯一的地址。我们可以从草案 C99 和 C11 标准第 6.5.9 节平等运算符中看到这一点:

两个指针比较相等当且仅当两者都是空指针,两者都是指向 相同的对象(包括指向对象的指针和开头的子对象)或函数, 两者都是指向同一数组对象的最后一个元素的指针,或者一个是指针 一个指向一个数组对象的末尾,另一个是指向另一个数组对象开头的指针 恰好紧跟在地址中的第一个数组对象之后的数组对象 空间.94)

因此,这需要一些特殊的外壳,并且可以使用其他方法提供大部分有用的功能,例如灵活数组。

它也可能需要在其他地方进行更改,如 M.M.在6.3.2.1 左值、数组和函数指示符中指出数组到指针的衰减:

[...]一个表达式 type ‘‘array of type’’ 被转换为类型为 ‘‘pointer to type’’ 的表达式,它指向 到数组对象的初始元素并且不是左值[...]

这似乎需要进行一些重要的更改才能获得最小的附加收益。

【讨论】:

"或者一个是指向一个数组对象末尾的指针,另一个是指向另一个数组对象的开头的指针,该数组对象恰好紧跟在第一个数组对象之后地址空间”意味着必须分配至少一个元素,但是,x 仍然可以是零,功能和实际,运行时现在将分配一个元素。如果我们期望 a 和 b 在内存中是连续的,并且想要计算 b 之后的 c 的地址为 c==a+sizeof(a)+sizeof(b),那么 UB 将跟随,因为 a 和 b 现在不是零大小(除非 sizeof 可以处理)。 它和malloc(0)有什么不同? @Jason from 7.20.3 如果请求的空间大小为零,则行为由实现定义:返回空指针,或者行为好像大小是一些非零值,但返回的指针不得用于访问对象 谢谢。实际上,定义的实现对我来说似乎更直观。不过,我不是编译器作者或语言维护者。【参考方案3】:

看C标准:

C11- 6.7.6.2 数组声明符(p1):

[...] 如果表达式是一个常量表达式,应该有一个大于零的值。 [...]

(p5):

如果大小是一个不是整数常量表达式的表达式:如果它出现在函数原型范围的声明中,则将其视为被*替换;否则,每次对其进行评估时,应具有大于零的值。 [...]

4。一致性:

如果违反了出现在约束或运行时约束之外的“应”或“不应”要求,则行为未定义。未定义的行为在本国际标准中以“未定义的行为”一词或省略任何明确的行为定义来表示。 这三者在侧重点上没有区别;它们都描述了“未定义的行为”

因此,声明一个大小为零的数组会导致程序的未定义行为。

【讨论】:

"因此,C 不允许数组长度为零。" - 为什么?那里的解释有很大的跳跃。 @KarolyHorvath;我不是说通俗地说吗?现在我正在等待你解释清楚的答案。 我只是指出我不明白你的解释。我希望你习惯于无限繁忙循环或无限阻塞 API 调用,因为我没有解释。并不是说我需要对您的模糊解释发表评论。 我们是否可以使用pointerToObject = &amp;vla[0] 之类的方式访问vla[0] @Jongware;不,我们不能。

以上是关于为啥零长度 VLA 是 UB?的主要内容,如果未能解决你的问题,请参考以下文章

在 MS Visual C++ 中启用 VLA(可变长度数组)?

C++ 可变长度数组 (VLA) 警告

C 和 C++ 中的可变长度数组 (VLA)

未记录的 GCC 扩展:结构中的 VLA

在其内存应该已被释放后访问可变长度数组

C99 式 VLA 都有哪些技术缺点? [关闭]