限制的正式定义未能考虑有效的情况

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),则指针表达式pp+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。然而,Tconst 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 肯定是错的。

原因如下:

  1. 在表达式 中*q = 2q是“基于” p(因为如果p在序列点处修改S为指向 的副本x,则条件p != p0将变为 true,从而更改 的值q)。
  2. 在表达式 中*p = 1p是“基于” q(因为如果q在序列点处修改S为指向 的副本x,则条件q != q0将变为 true,从而更改 的值p)。
  3. 在 的主体中f,通过指针表达式对任何对象的任何访问始终都是“基于”p和“基于” q。所以没有违反限制,既不是restrict p,也不是restrict q。请注意,限制限定指针不会在内部分配指针表达式f(因为p++q++实际上不会发生)。
  4. 由于不存在restrict违规,因此程序没有UB,不允许编译器进行优化。

在我看来,该标准未能以预期的方式定义“基于”。那么预期的方式是什么?我的意思是,显然有一个预期的含义,即允许 GCC 进行优化,并且我相信 GCC 在这种情况下在道德上是正确的。我想避免编写可能被错误优化的程序。

dbu*_*ush 8

您对 6.7.3.1p3 的措辞允许您提到的情况的评估是正确的。这种措辞也违背了本意。

其意图首先在第 6.7.3p8 节中提到:

通过限制限定指针访问的对象与该指针具有特殊关联。这种关联(在下面的 6.7.3.1 中定义)要求对该对象的所有访问都直接或间接地使用该特定指针的值。

所以所描述的q都是q-1基于对象q,而不是对象p

第 6.7.3.1p7 节给出了一个示例:

示例 1 文件范围声明

int * restrict a;
int * restrict b;
extern int c[];
Run Code Online (Sandbox Code Playgroud)

断言如果使用ab或之一访问某个对象c,并且该对象在程序中的任何位置被修改,则永远不会使用其他两个之一访问该对象

这进一步支持了这一意图。

通过严格阅读6.7.3.1p3,该表达式q-1基于对象pq,这似乎没有多大意义,并且与上面引用的描述和示例相反。

我认为 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在某些精心选择的位置更改 的值可能会导致执行不同的评估顺序,这一事实并不密切。在评估“基于”时,我们着眼于对所执行的评估的影响而不是这可能如何改变评估的顺序。

事实上,由于qq - 1并不是p规范所意图的“基于”,因此该程序不会违反与 -restrict限定类型相关的要求。


但同样的逻辑也可以反过来应用,即

……无关紧要,因为它首先就是错误的。

但让我们看看这个例子中的程序:

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)

pp0p != p0p++都是基于p,但又不基于q

qq0q != q0q++都是基于q,但又不基于p

任一*p*q可以填补规范的角色L,而另一个则restrict对一个底层对象执行违规修改。因此,该行为是未定义的,GCC 可以用它做任何它想做的事。

  • 关于“它对控制流变化引起的差异不感兴趣,如您所描述的”:我希望这是意图,但事实并非如此。OP 是正确的,并且遵循标准的措辞:他们提出了一种情况,其中“p”更改为指向数组的副本,然后探讨其后果,即控制流发生变化,因为“pEnd”不改变改变。这是标准中的缺陷,而不是OP的错误推理。 (2认同)