`sizeof`的操作数是否使用VLA进行评估?

25
这个答案的评论部分发生了一场争论,促使我提出了这个问题。
在下面的代码中,bar指向一个变长数组,因此sizeof是在运行时确定的,而不是编译时确定的。
int foo = 100;
double (*bar)[foo];

争论的焦点是当操作数是一个可变长度数组时,是否使用sizeof会评估其操作数,从而使得sizeof(*bar)bar未初始化时成为未定义行为。
因为我正在解引用一个未初始化的指针,所以使用sizeof(*bar)是否属于未定义行为?当类型是可变长度数组时,sizeof的操作数实际上是否被评估,还是仅确定其类型(就像sizeof通常的工作方式)?
编辑:似乎每个人都在引用C11草案中的这段文字。有人知道这是否是官方标准中的措辞吗?

2
@BLUEPIXY 问题在于,如果它是未定义行为,它仍然可能表现出您所期望的行为(即使跨平台和编译器)。 - PC Luddite
2
@BLUEPIXY 不需要,但问题实际上是关于它是否符合标准的未定义行为。 - PC Luddite
1
@BLUEPIXY,这可能没有意义,但这就是当前标准所规定的。 - PC Luddite
2
@BLUEPIXY 这个论点实际上是关于标准规定的内容,而不是实际发生的情况。 - PC Luddite
2
在抽象机中,所有表达式的求值均按照语义规定进行。实际实现不需要对一个表达式的一部分进行求值,如果它能推断出其值未被使用且没有产生任何所需的副作用(包括由调用函数或访问易失性对象引起的副作用)。 - BLUEPIXY
显示剩余12条评论
3个回答

14

是的,这会导致未定义行为。

在N1570 6.5.3.4/2中,我们有:

  

sizeof运算符返回其操作数的大小(以字节为单位),其可以是表达式或类型的括号名称。大小取决于操作数的类型。结果是整数。如果操作数的类型是变长数组类型,则对操作数进行评估;否则,不评估操作数,结果是整数常量。

现在我们有一个问题: *bar的类型是否为变长数组类型?

由于 bar 声明为指向VLA的指针,解引用它应该产生一个VLA。(但我没有看到具体说明它是否确实如此)。

注意:这里可能会进行进一步讨论,或许可以争辩说*bar的类型是double[100],而不是VLA

假设我们同意*bar的类型实际上是VLA类型,在sizeof *bar中,表达式*bar被评估。

此时bar是不确定的。现在来看6.3.2.1 / 1:

  

如果lvalue在评估时没有指定对象,则其行为未定义

由于bar不指向对象(因为是不确定的),评估*bar会导致未定义行为。


1
@iharob 嗯,double (*bar)[foo];声明了bar为指向VLA的指针,因此对它进行解引用应该会得到一个VLA。如果我们能够找到具体的文本来确认这一点那就更好了。 - M.M
2
int *pointer 之后,pointer 的类型是 int *,它不是 VLA 类型。*vla 的类型是 int,它也不是 VLA 类型,因此也不会被评估。 - M.M
1
好奇的讨论,我从未考虑过这可能是UB。在阅读6.7.6声明符时,有这样一句话:“完整声明符是不是另一个声明符的一部分的声明符。完整声明符的结尾是一个序列点。如果在完整声明符的嵌套声明符序列中有指定变长数组类型的声明符,则完整声明符指定的类型被称为可变修改的。此外,从可变修改类型派生的任何声明符类型派生的类型本身都是可变修改的。” - Lundin
1
@Olaf,“评估”和“取消引用”是不同的;例如,如果p是一个指针,那么p;会评估p但不会取消引用它。如果您将p替换为返回指针的函数,则差异可能更明显。然后调用该函数。 - M.M
2
@M.M:我非常清楚这一点!而且,这正是问题所在:为什么这两种情况必须要区别对待?VLA表达式必须被评估,但是产生VLA的(嵌套)表达式不需要:* f不需要被评估,只需解析以获取结果类型(例如int *),其是一个VLA,然后必须评估它(即索引)。如果* f不被评估,那么就没有未定义行为。 - too honest for this site
显示剩余16条评论

13

已经有两个答案引用了N1570 6.5.3.4p2:

 

sizeof操作符返回其操作数的大小(以字节为单位),操作数可以是表达式或类型名括号。大小取决于操作数的类型。结果是一个整数。如果操作数的类型是可变长度数组类型,则对操作数进行评估;否则,不评估操作数并且结果为整数常量。

根据标准中的这段代码,是的,sizeof的操作数会被评估。

我将争辩说,这在标准中是一个缺陷; 在运行时会评估某些内容,但不会评估操作数。

让我们考虑一个更简单的例子:

int len = 100;
double vla[len];
printf("sizeof vla = %zu\n", sizeof vla);

根据标准,sizeof vla会计算表达式vla的大小。但是这意味着什么?

在大多数情况下,评估数组表达式将产生初始元素的地址,但是sizeof运算符是明确的例外。我们可能会认为评估vla意味着访问其元素的值,但由于这些元素尚未初始化,因此具有未定义的行为。但是,在数组表达式的其他上下文中,没有任何其他情况涉及访问其元素的值,并且在这种情况下绝对不需要这样做。(更正:如果使用字符串字面值初始化数组对象,则会评估元素的值。)

当执行vla的声明时,编译器将创建一些匿名元数据来保存数组的长度(必须这样做,因为在定义和分配了vla后,将新值赋给len不会更改vla的长度)。只需通过sizeof (double)乘以存储的值(或者仅检索存储的值,如果它以字节为单位存储大小)即可确定sizeof vla

sizeof也可以应用于带括号的类型名称:

int len = 100;
printf("sizeof (double[len]) = %zu\n", sizeof (double[len]));
根据标准,sizeof表达式会计算出类型。这是什么意思?显然它要计算出当前len的值。另一个例子:

根据标准,sizeof表达式会计算出类型。这是什么意思?显然它要计算出当前len的值。另一个例子:

size_t func(void);
printf("sizeof (double[func()]) = %zu\n", sizeof (double[func()]));

这里的类型名包含函数调用。评估sizeof表达式必须调用该函数。

但在所有这些情况下,实际上没有必要评估数组对象的元素(如果有的话),也没有必要这样做。

sizeof应用于除VLA之外的任何东西都可以在编译时评估。当sizeof应用于VLA(对象或类型)时的区别在于,必须在运行时评估某些内容。但必须评估的是确定操作数大小所需的任何内容,而不是sizeof的操作数本身。

标准规定,如果sizeof的操作数是可变长度数组类型,则必须评估该操作数。这是标准中的缺陷。

回到问题中的示例:

int foo = 100;
double (*bar)[foo] = NULL;
printf("sizeof *bar = %zu\n", sizeof *bar);

我已经将对NULL的初始化添加到代码中,以使解引用bar具有未定义的行为变得更加清晰。

*bar的类型为double [foo],这是一个VLA类型。原则上,应该计算*bar的值,但由于bar未初始化,因此会产生未定义的行为。但是,没有必要解引用bar。编译器在处理double [foo]类型时将生成一些代码,包括将foo的值(或foo * sizeof(double))保存在匿名变量中。为了计算sizeof *bar,它只需检索该匿名变量的值即可。如果标准更新以一致的方式定义sizeof的语义,则可以清楚地了解评估sizeof *bar是良好定义的,并且可以在不必解引用bar的情况下生成100 * sizeof(double)的结果。


1
@PCLuddite:我的观点是这是标准中的一个缺陷。从字面上理解,它强制要求一些不必要的、毫无意义的操作,并且在某些情况下可能是不可能的(没有定义“评估”类型名称的含义),我相信这并不反映作者的意图。请参见此处以获取针对C11标准的缺陷报告列表。无论如何,我会稍微修改我的答案,称其为缺陷而不是“不正确”。 - Keith Thompson
2
@Olaf:不,我的意思是改变len的值不会改变vla的长度。我的意思是,假设有int len = 100; int vla[len]; len = 200;,将len的值更改为200不会影响vla的长度。我已经更新了我的答案以澄清这一点。 - Keith Thompson
2
@Olaf:我在2012年的comp.std.c新闻组上开始了这个问题的讨论;Google Groups档案在这里。它并没有真正走到任何地方 - 而且C标准委员会也没有与comp.std.c有任何官方联系。 - Keith Thompson
2
@PCLuddite:说实话,如果标准与常识相矛盾,而我使用的编译器确实遵循常识,被广泛使用,得到良好支持,并且不会将我锁定在特定的供应商上,那么我不会等待委员会解决问题,他们已经有了超过16年(或12年,计算最后两个版本之间的时间)的时间(这是我实际想法的礼貌版本)。Panta rhei. - too honest for this site
1
@M.M:我不同意。*bar 的类型是可变长数组(VLA)。类型不是绑定到名称,而是对象上。但在C语言中,这种绑定并没有存储在对象本身中,只有编译器知道,因此永远不需要访问对象本身来获取其类型,但是可以通过解析表达式和使用存储的元数据来推导出类型。这就是缺陷所在:"评估"这里应该意味着"解析"或"评估以获取类型",而不是读取值。只有后者(不必要的)才构成未定义行为。 - too honest for this site
显示剩余13条评论

4

实际上,标准似乎暗示行为未定义:

引用N1570 6.5.3.4/2:

sizeof操作符返回其操作数的大小(以字节为单位),该操作数可以是表达式或类型名称加括号。大小是从操作数的类型中确定的。结果是一个整数。如果操作数的类型是可变长度数组类型,则计算操作数的值;否则,不计算操作数的值,结果是一个整数常量。

我认为标准中的措辞令人困惑:计算操作数的值并不意味着*bar将被计算。评估*bar在任何方式下都无法帮助计算其大小。sizeof(*bar)确实需要在运行时计算,但为此生成的代码没有必要解引用bar,它更可能从一个隐藏的变量中检索大小信息,该变量保存了bar实例化时的大小计算结果。


我同意。请注意,gcc似乎表现如预期(再次)。 - too honest for this site

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