log*_*536 15 c language-lawyer restrict-qualifier
restirct根据C 标准中的正式定义,以下合理的程序似乎具有未定义的行为:
void positive_intcpy(int * restrict q, const int * restrict p, size_t n) {
int *qBgn = q;
const int *pEnd = p + n;
// sequence point S
while (p != pEnd && *p>0) *q++ = *p++;
if (q != qBgn) fprintf(stderr,"Debug: %d.\n",*(q-1)); // undefined behavior!?
}
int main(void) {
int a[6] = {4,3,2,1,0,-1};
int b[3];
positive_intcpy(b,a,3);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
该函数将整数从一个数组复制到另一个数组,只要整数为正数。该fprintf调用显示最后复制的正整数(如果有)。p和之间永远不会有任何混叠q。
这真的是UB,还是我的推理错误?
这个问题涉及C99标准的6.7.3.1节。C23最新草案中相关文本没有变化。
q-1我们讨论的是上面标记的指针表达式。是否基于 指定的受限指针对象p?
标准说:
在下文中,指针表达式
E被称为基于对象[PwhereP是限制限定的指针对象] if (在执行B[whereB是与 的声明相关联的块P] 的执行中的某个序列点,然后再评估E)修改P为指向它以前指向的数组对象的副本将更改E[参见脚注]的值。请注意,“based”仅针对具有指针类型的表达式定义。[脚注]换句话说,
E依赖于它本身的值P,而不是依赖于通过 间接引用的对象的值P。例如,如果标识符的p类型为(int **restrict),则指针表达式p和p+1基于 指定的受限指针对象p,但指针表达式*p和 则p[1]不是。
在我们的程序中,在S上面标记的序列点处,修改p为指向数组的副本a将导致p != pEnd始终为 true(因为pEnd不与 一起修改p),因此循环将执行直到变为 false,因此处*p>0的值q循环的结尾会改变(它会大一个机器字)。因此我们得出结论,我们的表达q-1是基于p.
现在标准说:
在 的每次执行期间
B,令为基于 的L任何左值。如果用于访问它指定的对象的值,并且也被修改(通过任何方式),则适用以下要求:[其中是声明指向的类型] 不应是 const 限定的。用于访问 的值的每个其他左值也应具有基于 的地址。就本子条款而言,每个修改的访问也应被视为修改。如果分配了基于与块关联的另一个受限指针对象的指针表达式的值,则 的执行应在 的执行之前开始,或者 的执行应在赋值之前结束。如果不满足这些要求,则行为未定义。&LPLXXTTPXPXPPEP2B2B2BB2
在我们的例子中,L是*(q-1). X是 处的对象&b[2],已将其修改为B的值2。然而,T是const int,表示该程序有UB。
您可能会说这是无害的,因为编译器可能不会利用如此难以证明的 UB 情况,因此不会对其进行错误优化。
但同样的逻辑可以反过来应用,这会变得非常危险:
int f(int *restrict p, int *restrict q) {
int *p0=p, *q0=q;
// sequence point S
if (p != p0) q++;
if (q != q0) p++;
*p = 1;
*q = 2;
return *p + *q;
}
int main(void) {
int x;
printf("%d\n", f(&x,&x));
return 0;
}
Run Code Online (Sandbox Code Playgroud)
GCC确实在这里进行了优化。GCC -O0打印4,同时GCC -O3打印3。不幸的是,从字面上看标准,GCC 肯定是错的。
原因如下:
*q = 2,q是“基于” p(因为如果p在序列点处修改S为指向 的副本x,则条件p != p0将变为 true,从而更改 的值q)。*p = 1,p是“基于” q(因为如果q在序列点处修改S为指向 的副本x,则条件q != q0将变为 true,从而更改 的值p)。f,通过指针表达式对任何对象的任何访问始终都是“基于”p和“基于” q。所以没有违反限制,既不是restrict p,也不是restrict q。请注意,限制限定指针不会在内部分配指针表达式f(因为p++和q++实际上不会发生)。restrict违规,因此程序没有UB,不允许编译器进行优化。在我看来,该标准未能以预期的方式定义“基于”。那么预期的方式是什么?我的意思是,显然有一个预期的含义,即允许 GCC 进行优化,并且我相信 GCC 在这种情况下在道德上是正确的。我想避免编写可能被错误优化的程序。
您对 6.7.3.1p3 的措辞允许您提到的情况的评估是正确的。这种措辞也违背了本意。
其意图首先在第 6.7.3p8 节中提到:
通过限制限定指针访问的对象与该指针具有特殊关联。这种关联(在下面的 6.7.3.1 中定义)要求对该对象的所有访问都直接或间接地使用该特定指针的值。
所以所描述的q都是q-1基于对象q,而不是对象p。
第 6.7.3.1p7 节给出了一个示例:
示例 1 文件范围声明
Run Code Online (Sandbox Code Playgroud)int * restrict a; int * restrict b; extern int c[];断言如果使用
a、b或之一访问某个对象c,并且该对象在程序中的任何位置被修改,则永远不会使用其他两个之一访问该对象
这进一步支持了这一意图。
通过严格阅读6.7.3.1p3,该表达式q-1基于对象p和q,这似乎没有多大意义,并且与上面引用的描述和示例相反。
我认为 6.7.3.1p3 的目的是捕获传递表达式,如下所示:
int x[5];
int * restrict p = x;
int *p1 = p+2; // p1 is based on p
int *p2 = p1-1; // p2 is also based on p
Run Code Online (Sandbox Code Playgroud)
因为简单地陈述“一个表达式包括p其操作数”并不能解释这种情况。
6.7.3.1p3 的可能改写如下:
如果以下任一条件成立,则指针表达式E被称为基于对象P :
- E包含P作为操作数
- E包含左值V作为操作数,并且V被设置为基于P 的表达式E2的值。
Joh*_*ger -1
这真的是UB,还是我的推理错误?
您的推理与规范的意图不一致。
S在我们的程序中,在上面标记的序列点处,修改p为指向a数组的副本将导致p != pEnd始终为 true (因为pEnd不与 一起修改p)
但这以及由此得出的推理并不是规范试图描述的内容。它对控制流变化引起的差异不感兴趣,例如您所呈现的那些。实际上很难准确描述,所以也许你可以原谅委员会,但这里是一个尝试:
考虑程序的执行,并查看具有影响某些表达式计算值的副作用的所有表达式计算E。
p想象一下,按照描述修改 的值,并再次执行所有相同的评估,考虑到p值的变化以及通过结果传播的差异,但不选择更多、更少或不同的表达式来评估,也不在不同的顺序。
如果 的最终评估E产生不同的值,则E基于p。
我认为这并不完美,但我希望它足以解释规范的含义和您所呈现的内容之间的差异。
有鉴于此,我希望您看到,p在某些精心选择的位置更改 的值可能会导致执行不同的评估顺序,这一事实并不密切。在评估“基于”时,我们着眼于对所执行的评估的影响,而不是这可能如何改变评估的顺序。
事实上,由于q和q - 1并不是p规范所意图的“基于”,因此该程序不会违反与 -restrict限定类型相关的要求。
但同样的逻辑也可以反过来应用,即
……无关紧要,因为它首先就是错误的。
但让我们看看这个例子中的程序:
Run Code Online (Sandbox Code Playgroud)int f(int *restrict p, int *restrict q) { int *p0=p, *q0=q; // sequence point S if (p != p0) q++; if (q != q0) p++; *p = 1; *q = 2; return *p + *q; } int main(void) { int x; printf("%d\n", f(&x,&x)); return 0; }
p、p0、p != p0和p++都是基于p,但又不基于q。
q、q0、q != q0和q++都是基于q,但又不基于p。
任一*p或*q可以填补规范的角色L,而另一个则restrict对一个底层对象执行违规修改。因此,该行为是未定义的,GCC 可以用它做任何它想做的事。