对可变长度数组执行 sizeof 操作 - 这样做有什么好处吗?

3

我正在处理一段旧代码(没有测试)。我无意中发现了几个宏里隐藏的一段代码。如果使用GCC的-Wvla编译,则会生成警告。

这段代码类似于以下小程序的内容:

typedef struct entry {
    unsigned index;
    unsigned reserved;
    unsigned value;
} entry_t;

int main(int argc, char **argv) {
    long pa = 0;
    long res = pa + sizeof(entry_t[10 - argc]);
    return res;
}

编译时会发出警告:
$ gcc -g -Wvla repro-vla.c
repro-vla.c: In function ‘main’:
repro-vla.c:9:5: warning: ISO C90 forbids variable length array [-Wvla]
    9 |     long res = pa + sizeof(entry_t[10 - argc]);
      |     ^~~~

问题的罪魁祸首是这个表达式:sizeof(entry_t[10 - argc])。语法有点令人困惑。我认为一个临时匿名数组被创建,类型为entry_t,包含 10-argc 个元素。然后它的大小被计算出来,并且这个数组会被丢弃。

我的问题如下:

  1. 我对代码的理解是否正确?
  2. 这个表达式与sizeof(entry_t) * (10-argc)有何不同?两者计算出相同的值,都没有防止下溢(当argc> = 10 时)。第二个表达式不使用变长数组,因此不会生成警告,并且在我看来更易于理解。

1
不创建数组。 - chux - Reinstate Monica
1
@ti7 不要,先读他的问题再给链接,不要没看帖子就发链接。 - Fredrik
2
"ISO C90禁止使用可变长度数组。我建议忘记30年的标准。" - 0___________
1
@0___________ "入乡随俗"。我需要为一个Linux内核模块适应代码。该代码库使用它自己的一套编译规则。其中许多规则被认为是陈旧的或者是产生反效果的。但这是另一个讨论的话题,会在其他地方讨论。当然,我同意GCC的"-Wvla"警告的表达方式有一定的问题。 - Grigory Rechistov
@0___________ 在我看来,代码被移植到Linux并不会改变问题的本质。当然,这可能会使得-Wvla被添加到编译标志列表中更有分量。我更关心的是我是否正确理解了代码,因为sizeof(VLA type)并不是我每天都能观察到的东西。下面的答案非常好地回答了我的疑虑! - Grigory Rechistov
显示剩余9条评论
3个回答

5

不会创建任何临时数组对象。

有一个普遍的误解,即VLA是关于在运行时定义长度的堆栈分配数组的一种形式,类似于更安全的alloca()

不是这样的。VLA关注的是类型而不是存储。以下行是“VLA-ness”的本质:

typedef int T[n];

不是:

int A[n];

请注意,VLA类型声明不会分配任何存储空间,因此可以放心使用,而不必担心堆栈溢出。 VLA类型非常有用,可用于处理多维数组并在函数参数中表示访问范围,例如:void foo(int n, int arr[n]);。 VLA类型在C11中是可选的,但由于其有用性,它们将是C23中必需的。
表达式sizeof(entry_t [10-argc]本质上与以下内容相同:
typedef entry_t _unnamed_type[10 - argc];
sizeof(_unnamed_type)

那里没有创建任何VLA数组对象。我认为问题在于-Wvla标志本身。 -Wvla警告任何声明VLA类型的情况(而不是VLA对象),这有点过于热心了,因为它也会捕捉到VLA类型的良好使用方式。这里有一个request要求将-Wvla-stack-allocation警告添加到clang中,以捕捉VLA的危险用法。或者,可以使用gcc的-Wvla-larger-than=0,但它的效果不太好。


"VLAs关乎类型而非存储。" 这是一个很好的解释,恰好是我在理解它时所缺失的东西。谢谢! - Grigory Rechistov

3
获取可变长度数组的sizeof - 这样做有什么好处吗?
使用 sizeof(entry_t) * (10-argc) 时,当 argc >= 10 时,它具有定义的结果*1。但是,当使用 sizeof(entry_t[10-argc]) 时,在这种情况下它是未定义行为(UB),因为它未能满足“如果大小是不是整数常量表达式:...每次评估时它都应该有一个大于零的值。”C17dr § 6.7.6.2 5(数组声明符)。
下一步代码将其转换为 long ,然后再转换为 int ,当 sizeof(entry_t) * (10-argc) 大于 LONG_MAX,INT_MAX 时,其实现已被定义的行为。
小优势

给定argc >= 10的UB,优化编译器可以假设argc < 10并发出利用返回值仅在一个狭窄范围内(例如:[3*4 ... 3*4*9])的代码 - 因此只需要使用狭窄整数运算。没有太多的优势 - 但是它就在那里。


*1 肯定类型 size_t


好的观点。用另一个代替一个正在影响边缘情况。以前是未定义的,现在变得明确定义了,尽管从我正在处理的高级逻辑的角度来看仍然不正确。原则上,运行时也可以捕获到达此UB的情况。实际上,这并没有被执行。因此,消除VLA类型的转换应该是有益的,因为它使代码更容易理解。 - Grigory Rechistov
@GrigoryRechistov,你的问题让我想起了1900年2月29日的错误。早期的一个错误没有被修复以保持兼容性。 - chux - Reinstate Monica

3
在这个表达式中,sizeof(entry_t[10 - argc])并没有创建任何数组。它会计算表达式10 - argc的值,并根据entry_t的类型计算出这样一个数组的大小。entry_t[10 - argc]是一种类型说明符而不是表达式。因此,例如你不能写成:
sizeof entry_t[10 - argc]

这些表达式 sizeof(entry_t[10 - argc])sizeof(entry_t) * (10-argc) 产生相同的值,因为数组的大小等于数组元素数量乘以其元素的大小,前提是 10 大于 argc

关于 VLA 中元素数量,C 标准有一个限制条件,即 “每次计算时应具有大于零的值。”

请注意,在表达式 sizeof(entry_t) * (10-argc) 中,当 argc 大于 10 时,由于通常的算术转换,可以获得类型为 size_t 的非常大的值。

还有一行代码:

long res = pa + sizeof(entry_t[10 - argc]);

引发了一个问题,为什么将无符号整数类型size_t的值分配给有符号整数类型long的变量。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接