将两个与同一数组无关的指针相减的基本原理是什么?

αλε*_*λυτ 16 c++ pointer-arithmetic language-lawyer

根据C ++草案expr.add,当您减去相同类型但不属于同一数组的指针时,其行为是不确定的(强调是我的):

当两个指针表达式P和Q相减时,结果的类型为实现定义的有符号整数类型;此类型应与在标头([support.types])中定义为std :: ptrdiff_t的类型相同。

  • 如果P和Q都得出空指针值,则结果为0。(5.2)
  • 否则,如果P和Q分别指向同一数组对象x的元素x [i]和x [j],则表达式P-Q具有值i?j。

  • 否则,行为是不确定的。 [?注意:如果值i?j不在std :: ptrdiff_t类型的可表示值的范围内,则行为是不确定的。-?尾注?]

将此类行为设为未定义(而不是由实现定义)的原理是什么?

Lig*_*ica 16

从学术上讲:指针不是数字。它们是指针。

的确,系统上的指针是作为某种抽象类型的内存(可能是虚拟的,按进程的内存空间)中某个位置的类似地址的表示形式的数字表示形式而实现的。

但是C ++对此并不关心。C ++希望您将指针视为指向特定对象的便笺,书签。数字地址值只是一个副作用。对指针有意义的唯一算法是通过对象数组前后移动。在哲学上没有别的意义。

这看起来似乎很神秘,没有用,但是实际上是有意的和有用的。C ++不想将实现限制于它无法控制的实用,低级计算机属性上赋予更多含义。而且,由于没有理由这样做(为什么要这样做?),它只是说结果是不确定的。

在实践中,您可能会发现减法有效。但是,编译器极其复杂,并且为了尽可能快地生成代码,大量使用了标准规则。当您违反规则时,这可能并且经常会导致您的程序出现奇怪的事情。如果在编译器假设原始值和结果都引用同一数组的情况下,如果指针算术运算被破坏,不要感到惊讶,这是您违反的假设。


P.W*_*P.W 8

正如评论中的某些人所指出的那样,除非结果值具有某种含义或可以某种方式使用,否则没有必要定义行为。

对于C语言,已经进行了一项研究,以回答与Pointer Provenance有关的问题(并打算对C规范提出措辞更改。),其中一个问题是:

一个对象可以通过对象间减法(使用指针或整数算术)在两个单独分配的对象之间建立一个可用的偏移量,从而通过在第一个对象上添加偏移量来使第二个对象成为可用的指针?(资源)

研究作者的结论发表在一篇名为《探索C语义学和指针源》的论文中,对于这个特定问题,答案是:

对象间指针算法 本节中的第一个示例依靠猜测(然后检查)两个分配之间的偏移量。如果改为用指针减法计算偏移量,该怎么办?是否应该让一个物体在下面移动?

// pointer_offset_from_ptr_subtraction_global_xy.c
#include <stdio.h>
#include <string.h>
#include <stddef.h>

int x=1, y=2;
int main() {
    int *p = &x;
    int *q = &y;
    ptrdiff_t offset = q - p;
    int *r = p + offset;
    if (memcmp(&r, &q, sizeof(r)) == 0) {
        *r = 11; // is this free of UB?
        printf("y=%d *q=%d *r=%d\n",y,*q,*r);
    }
}
Run Code Online (Sandbox Code Playgroud)

在ISO C11中,q-pis UB(作为指向不同对象的指针之间的指针减法,在某些抽象机执行中,这些对象与过去不相关)。在允许构造多个过去的指针的一种变体语义中,必须选择*r=11访问是否为UB。基本的出处语义将禁止它,因为r将保留x分配的出处,但是它的地址并不限于此。这可能是最理想的语义:我们发现很少有示例语言惯于使用对象间指针算法,并且禁止它给别名分析和优化的自由似乎很重要。

C ++社区对这项研究进行了总结,然后将其发送给WG21(C ++标准委员会)以征询反馈。

摘要要点

指针差仅针对具有相同出处和相同阵列内的指针定义。

因此,他们决定暂时保持不确定状态。

请注意,C ++标准委员会中有一个研究组SG12,用于研究未定义的行为和漏洞。该小组进行了系统的审查,以对标准中的漏洞和未定义/未指定行为的案例进行分类,并推荐一组连贯的更改来定义和/或指定行为。您可以跟踪该小组的程序,以查看将来是否会对当前未定义或未指定的行为进行任何更改。


eer*_*ika 5

首先,请参阅注释中提到的这个问题,以了解为什么它的定义不明确。简要给出的答案是,在某些(现在是过时的)系统使用的分段存储器模型中,不可能执行任意指针运算。

取消定义此类行为而不是定义实现的理由是什么?

每当标准将某些东西指定为未定义的行为时,通常只能将其指定为实现定义。那么,为什么要指定任何未定义的内容呢?

好吧,未定义的行为更宽松。特别是,允许​​假设没有未定义的行为,如果假设不正确,编译器可能会执行优化操作,从而破坏程序。因此,指定未定义行为的原因是优化。

让我们考虑fun(int* arr1, int* arr2)将两个指针作为参数的函数。这些指针可以指向或不指向同一数组。假设该函数迭代一个指向数组(arr1 + n),并且(arr1 + n) != arr2在每次迭代中必须将每个位置与另一个指针进行比较,以得出相等性()。例如,确保不覆盖指向的对象。

比方说,我们称这样的功能:fun(array1, array2)。编译器知道(array1 + n) != array2,因为否则行为是不确定的。因此,如果将函数调用内联扩展,则编译器可以删除(arr1 + n) != arr2始终为真的冗余检查。如果跨数组边界的指针算术定义得很好(甚至是实现),那么(array1 + n) == array2使用some可能是正确的n,并且这种优化将是不可能的-除非编译器可以证明(array1 + n) != array2对所有可能值的保持n有时很难证明。


即使在分段内存模型中,也可以实现跨类成员的指针算术。在子数组的边界上进行迭代也是如此。在某些用例中,它们可能非常有用,但从技术上讲,它们是UB。

在这些情况下,对于UB的争论是UB优化的更多可能性。您不一定需要同意这是足够的论点。