`sizeof` 的操作数是用 VLA 评估的吗?

Posted

技术标签:

【中文标题】`sizeof` 的操作数是用 VLA 评估的吗?【英文标题】:Is the operand of `sizeof` evaluated with a VLA? 【发布时间】:2016-01-04 06:47:50 【问题描述】:

this answer 的 cmets 部分中的一个论点促使我提出这个问题。

在下面的代码中,bar 指向一个变长数组,所以sizeof 是在运行时而不是编译时确定的。

int foo = 100;
double (*bar)[foo];

争论是关于当操作数是可变长度数组时是否使用sizeof 评估其操作数,使得sizeof(*bar)bar 未初始化时的行为未定义。

使用sizeof(*bar) 是否是未定义的行为,因为我正在取消引用未初始化的指针? sizeof 的操作数是在类型是可变长度数组时实际计算的,还是只是确定其类型(sizeof 通常如何工作)?


编辑:每个人似乎都在引用 C11 草案中的 this passage。有谁知道这是否是官方标准中的措辞?

【问题讨论】:

@BLUEPIXY 但问题是,如果它是未定义的行为,它仍然可能像您期望的那样运行(即使跨平台和编译器)。 @BLUEPIXY 不,不需要,但问题实际上是关于根据标准它是否是未定义的行为。 @BLUEPIXY 这可能没有意义,但这就是标准目前所说的。 @BLUEPIXY 这个争论实际上是关于标准所说的,而不是实际发生的。 5.1.2.3/4: "在抽象机中,所有表达式都按照语义指定的方式进行计算。如果可以推断出表达式的值,则实际实现不需要计算表达式的一部分未使用并且不会产生任何需要的副作用(包括由调用函数或访问 volatile 对象引起的任何副作用)。” 【参考方案1】:

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

在 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:

如果一个左值在计算时没有指定一个对象,则 行为未定义

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

【讨论】:

@iharob 好吧,double (*bar)[foo];bar 声明为指向 VLA 的指针,因此取消引用它应该给出一个 VLA。如果我们能找到具体的文字来确认就很好了 int *pointer之后,pointer的类型是int *,它不是VLA类型。 *vla 的类型为 int,它不是 VLA 类型,因此也不对其进行评估。 好奇的讨论,我从没想过这可能是UB。阅读 6.7.6 声明符,有这样的声明:A full declarator is a declarator that is not part of another declarator. The end of a full declarator is a sequence point. If, in the nested sequence of declarators in a full declarator, there is a declarator specifying a variable length array type, the type specified by the full declarator is said to be variably modified. Furthermore, any type derived by declarator type derivation from a variably modified type is itself variably modified. @Olaf “评估”和“取消引用”是不同的;例如如果p 是一个指针,则p; 会评估p 但不会取消引用它。如果将p 替换为返回指针的函数,则差异可能会更加明显。然后调用该函数。 @M.M:我很清楚!而正是的问题是:为什么这两种情况必须区别对待? VLA 表达式 必须 被评估,但不是产生 VLA 的(嵌套)表达式:*f 不需要评估,而只需解析以获得结果类型(如int *) ,这是一个 VLA,然后必须对其进行评估(即索引)。如果*f 不会被评估,则没有UB。【参考方案2】:

事实上,标准似乎暗示行为是未定义的:

重新引用 N1570 6.5.3.4/2:

sizeof 运算符产生其操作数的大小(以字节为单位),它可以是表达式或类型的括号名称。大小由操作数的类型决定。结果是一个整数。如果操作数的类型是变长数组类型,则计算操作数;否则,不计算操作数,结果为整数常量。

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

【讨论】:

我同意。请注意,gcc 的行为似乎符合预期(再次)。【参考方案3】:

另外两个答案已经引用N15706.5.3.4p2:

sizeof 运算符产生其操作数的大小(以字节为单位),即 可以是表达式或类型的括号名称。尺寸是 由操作数的类型决定。结果是一个整数。如果 操作数的类型是变长数组类型,操作数 被评估;否则,不计算操作数并且结果 是一个整数常量。

根据标准中的那一段,是的,sizeof 的操作数被评估。

我要争辩说这是标准中的一个缺陷; something 在运行时被评估,但操作数不是。

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

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

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

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

vla 的声明被执行时,编译器将创建一些匿名元数据来保存数组的长度(它必须这样做,因为在定义和分配vla 之后为len 分配一个新值不会'不要改变vla的长度)。确定sizeof vla 所需要做的就是将该存储的值乘以sizeof (double)(或者如果它以字节为单位存储大小,则只需检索存储的值)。

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

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

根据标准,sizeof 表达式计算 类型。那是什么意思?显然它必须评估len 的当前值。另一个例子:

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

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

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

sizeof 应用于 VLA 以外的任何内容都可以在编译时进行评估。 sizeof 应用于 VLA(对象或类型)时的区别在于 something 必须在运行时进行评估。但是要求值的不是sizeof的操作数;它只是确定操作数大小所需的任何内容,而不是操作数本身。

标准规定,如果sizeof 的操作数是可变长度数组类型,则计算该操作数。这是标准中的一个缺陷。

回到问题中的例子:

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

我已向 NULL 添加了一个初始化,以更清楚地说明取消引用 bar 具有未定义的行为。

*bardouble[foo] 类型,它是一个 VLA 类型。原则上,*bar 被评估,这将具有未定义的行为,因为 bar 未初始化。但同样,没有必要取消引用bar。编译器在处理double[foo] 类型时会生成一些代码,包括将foo(或foo * sizeof (double))的值保存在匿名变量中。评估sizeof *bar 所要做的就是检索该匿名变量的值。如果更新标准以定义sizeof 的语义一致,很明显,评估sizeof *bar 是明确定义的并且产生100 * sizeof (double) 而无需取消引用bar

【讨论】:

@PCLuddite:我的论点是这是标准中的一个缺陷。从字面上看,它强加了一个不必要的要求,没有任何意义,在某些情况下这可能是不可能的(没有定义“评估”类型名称的含义),而且我有理由确定确实如此不反映作者的意图。请参阅here 以获取针对 C11 标准的缺陷报告列表。无论如何,我会稍微改一下我的回答,说这是一个缺陷,而不是“不正确”。 @Olaf:不,我的意思是改变len 的值不会改变vla 的长度。我的意思是给定int len = 100; int vla[len]; len = 200;,将len 的值更改为200 不会影响vla 的长度。我已经更新了我的答案以澄清这一点。 @Olaf:我在 2012 年开始在 comp.std.c 新闻组上讨论这一点; Google 网上论坛存档为 here。它并没有真正去任何地方——而且 C 标准委员会与 comp.std.c 没有任何官方隶属关系。 @PCLuddite:老实说:如果标准与常识相矛盾,我使用的编译器实际上确实遵循,被广泛使用,得到很好的支持,并且不会将我锁定到特定的供应商,我不会等到委员会弄清楚他们有超过 16 年(或 12 年,计算最后两个版本之间的时间)的时间(这是我实际想法的礼貌版本)。潘塔雷伊。 @MM:我不同意。 *bar 的类型是 VLA。类型不绑定到名称,而是绑定到对象。但是在 C 中,这个绑定不与对象本身一起存储,而只有编译器知道,所以永远不需要访问对象本身来获取它的类型,但这总是可以通过解析表达式并使用 sotred元数据。这就是缺陷所在:这里的“评估”应该意味着“解析”或“评估以获取类型”,而不是读取值。只有(不必要的)后者实际上构成了 UB。

以上是关于`sizeof` 的操作数是用 VLA 评估的吗?的主要内容,如果未能解决你的问题,请参考以下文章

具有可变长度数组类型的 Sizeof 运算符

使用指针来实现变长数组(VLA)

sizeof 是在编译时还是运行时评估?

不评估应用了 sizeof 的表达式是不是使得在 C++ 中取消引用 sizeof 内的空指针或无效指针是合法的?

评估一个静态私有变量(Java),不应该是非法的吗? [复制]

如果存在 VLA,为啥仍然需要 malloc? [复制]