这个 gcc "sequence point" UB 警告在 C 中有效吗?


#include <stdio.h>
int main(void)
{
char buf[100], *p = buf;
p = buf + sprintf(p=buf, "%d", 123);
return 0;
}

使用 gcc 9.3.0 或 12.1.0 和 -std=c17(或 c11/c99/c89),我得到:warning: operation on ‘p’ may be undefined [-Wsequence-point]指着p = buf +...中的=. 我看到 p 在一个表达式中被赋值两次,但在函数调用中的赋值后有一个序列点。这可能是未定义的行为吗?

代码最初是p += sprintf(p=buf, "%d", 123);的。这得到了同样的警告,指向+=.这是同样的情况吗?这是 UB 吗?

我们关心的问题是,根据C 2018 6.5 2,p是否在没有测序的情况下被修改了两次:

如果标量对象的副作用相对于同一标量

对象上的不同副作用或使用同一标量对象的值计算未排序,则行为是未定义的...

如下所述,GCC 抱怨因此可能存在未定义的行为是错误的,前提是确保sprintf是函数调用。根据 7.1.4 1,在标准库标头中声明的任何函数都可以另外实现为类似函数的宏。这意味着sprintf可能在p = buf + sprintf(p=buf, "%d", 123);中被宏观替换,产生一些在p=buf和较大p = buf +…之间没有测序的表达。但是,即使通过将sprintf括在括号中来抑制这种可能性,GCC 也会抱怨,使用p = buf + (sprintf)(p=buf, "%d", 123);.

有问题的p的两次访问是作为分配的副作用执行的更新。赋值计算其左操作数的左操作数,计算其右操作数,并作为副作用更新其左操作数引用的对象。前两个评估是无序的,但是评估左操作数的左操作数的左值既不是对引用对象的副作用,也不是使用其值的值计算,因此我们不关心它。(评估p的净值是微不足道的;p是指名为"p"的对象。在表达式(如q[3+f(x)])中计算表达式的左值更为复杂, 必须计算下标表达式,如果q是指针而不是数组,则检索其值。然后计算最终地址,该地址用于被分配到的对象的左值。

对于作业,C 2018 6.5.16 3 说:

。更新左操作数的存储值的副作用是在左操作数和右操作数的值计算之后排序的......

这意味着较大赋值中p的更新是在操作数的值计算之后排序的,但它并没有告诉我们更新是在p=buf更新之后排序的。

但是,p=buf出现在sprintf的论点中。C 2018 6.5.2.2 10 告诉我们:

函数指示符和实际参数的计算之后,但在实际调用之前有一个序列点......

根据 5.1.2.3 3,"...表达式 A 和 B 的计算之间存在一个序列点,这意味着与A相关的每个值计算和副作用都是在与B相关的每个值计算和副作用之前排序的......

这里有一个问题。我们可以很容易地将 A 作为p=buf,并且在这个 A 和对sprintf的调用之间有一个序列点,因此与A相关的副作用是在调用sprintf之前排序的。但是,我们使用什么来确保较大赋值p = buf + …的副作用是在序列点之后?我们不能把 B 本身看成是这个更大的赋值,因为那样序列点就不会在AB之间;它会在A之后但在B内部的某个地方。

我的解释是,这是标准措辞上的缺陷。如果我们需要从字面上识别一些完全独立的表达式B来应用关于序列点的规则,那么在函数调用之前存在序列点会失去大部分效果。说参数的评估和字面上的"实际调用"之间存在一个序列点没有多大意义,因为"实际调用"不是一个表达式。因此,关于表达式之间序列点的措辞必须适用于表达式的某些部分或在评估表达式过程中完成的操作。

如果sprintf是一个普通的例程而不是一个库例程,这将不是问题。在普通例程中,函数内部有表达式,序列点意味着我们的A(p=buf)在这些表达式之前被排序,而这些表达式又在较大p = buf + …p更新之前被排序,因为它们是=的右操作数的一部分,其计算是在更新p的副作用之前排序的。但是,C 库例程是"整体"指定的,作为执行声明效果的特殊例程,而不是作为普通的 C 代码。尽管如此,我的解释是,参数p=buf旨在在函数调用之前完成,并作为较大p = buf +…的右操作数的一部分,因此它的副作用在该较大赋值中的副作用之前排序。

相关内容

最新更新