在特定实现上定义了两个数组之间的指针差异吗?

Bér*_*ger 6 c c++ pointer-arithmetic language-lawyer

根据C标准:

当两个指针相减时,两者都应指向同一数组对象的元素,或指向数组对象最后一个元素的元素(第 6.5.6 1173 节)

[注意:不要假设我对标准或 UB 了解很多,我只是碰巧发现了这个]

  1. 我知道在几乎所有情况下,无论如何取两个不同数组中指针的差异都是一个坏主意。
  2. 我也知道在某些架构上(我在某处读到的“分段机器”),行为未定义有充分的理由。

现在另一方面

  1. 它在某些极端情况下可能有用。例如,在这篇文章中,它将允许使用具有不同数组的库接口,而不是复制一个数组中的所有内容,然后将其拆分。
  2. 似乎在“普通”架构上,“所有对象都存储在一个大数组中,从大约 0 开始到大约内存大小”的思维方式是对内存的合理描述。当您实际查看不同数组的指针差异时,您会得到合理的结果。

因此我的问题是:从实验来看,似乎在某些体系结构(例如 x86-64)上,两个数组之间的指针差异提供了合理的、可重现的结果。它似乎与这些架构的硬件相当吻合。那么某些实现是否真的确保了特定的行为?

例如,在野外是否有一个实现可以保证a并且b存在char*,我们有a + (reinterpret_cast<std::ptrdiff_t>(b)-reinterpret_cast<std::ptrdiff_t>(a)) == b

Dev*_*lar 6

为什么使它成为 UB,而不是实现定义?(当然,对于某些架构,实现定义会将其指定为 UB)

这不是它的工作原理。

如果标准将某些内容记录为“实现定义”,那么任何符合要求的实现都应该为该情况定义行为,并记录下来。让它未定义不是一种选择。

由于不相关数组之间的标记指针差异“实现定义”会使分段哈佛架构无法获得完全一致的实现,因此该标准仍未定义这种情况。

实现可以提供定义的行为作为非标准扩展。但是任何使用这种扩展的程序将不再严格遵守,并且不可移植。

  • ...和[哈佛架构](https://en.wikipedia.org/wiki/Harvard_architecture)。当一个数组位于代码存储器中,另一个数组位于数据存储器中时,两个数组地址之间的差异是没有意义的。 (2认同)

Ant*_*ala 5

任何实现都可以自由地记录标准不要求记录行为的行为 - 它完全在标准的限制范围内。在这种情况下,实现定义行为的问题在于,实现必须仔细记录它们,当 C 被标准化时,委员会大概发现不同的实现变化如此之大,不存在合理的共同点,所以他们决定完全把它变成UB。


我不知道任何的编译器让它定义,但我知道一个编译器,其不明确保持不确定,即使你试图用石膏欺骗:

当从指针转换为整数并再次返回时,结果指针必须引用与原始指针相同的对象,否则行为未定义。也就是说,不能使用整数算术来避免 C99 和 C11 6.5.6/8 中禁止的指针算术的未定义行为。

我相信另一个编译器也有相同的行为,但不幸的是它没有以可访问的方式记录它

这两个编译器没有定义这将是避免在任何程序中依赖它的一个很好的理由,即使使用另一个会指定行为的编译器编译,因为您永远不会太确定从 5 年起需要使用什么编译器现在...


Nic*_*las 4

您拥有的实现定义的行为以及某人的代码所依赖的行为越多,代码的可移植性就越差。在这种情况下,已经有一种实现定义的方法来解决这个问题:reinterpret_cast指向整数的指针并在那里进行数学运算。这让每个人都清楚,您依赖于特定于实现的行为(或者至少是可能无法在任何地方移植的行为)。

另外,虽然运行时环境实际上可能是“所有对象都存储在一个大数组中,从大约 0 开始,到大约内存大小结束”,但编译时行为并非如此。在编译时,您可以获得指向对象的指针并对它们进行指针算术。但是,将此类指针仅视为内存中的地址可能允许用户开始对编译器数据等进行索引。通过使这些东西成为 UB,它在编译时明确禁止(并且reinterpret_cast在编译时明确禁止)。