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

问题描述

根据C标准:

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

[注意:不要以为我对标准或UB了解很多,我只是碰巧发现了这一点]

  1. 我了解几乎在所有情况下,将指针的不同放在两个不同的数组中都是个坏主意。
  2. 我还知道,在某些体系结构(如我在某处阅读的“分段机”)中,有充分的理由说明行为是不确定的。

另一方面,现在

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

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

例如,是否有一种狂野的实现可以保证abchar*,我们有a + (reinterpret_cast<std::ptrdiff_t>(b)-reinterpret_cast<std::ptrdiff_t>(a)) == b

解决方法

为什么将其设为UB,而不是实现定义的? (当然,对于某些体系结构,实现定义的会将其指定为UB)

那不是它的工作方式。

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

由于“实现定义”的不相关数组之间的标签指针差异将导致例如segmentedHarvard体系结构无法实现完全一致的实现,根据标准,此情况仍然未定义

实施可以提供定义的行为作为非标准扩展。但是任何使用这种扩展的程序都将不再严格合规且不可移植。

,

任何实施都是免费的,用于记录行为,而该行为不需要该标准予以记录-完全在该标准的范围之内。在这种情况下,实现定义的行为存在的问题是,实现必须随后仔细记录它们,并且在对C进行标准化时,委员会大概会发现不同的实现是如此多变,以至于没有明智的共识,因此它们决定完全将其设为UB。


我不知道任何这样做的编译器,但是我know a compiler明确地使它保持未定义状态,即使您尝试使用类型转换作弊:

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

我相信another compiler的行为也一样,但不幸的是,它document it in an accessible way却没有。

这两个编译器没有定义,这是避免在任何程序中都依赖它的一个很好的理由,即使使用另一个将指定行为的编译器进行编译也是如此,因为您永远都不能这么做确定从现在开始需要使用5年的编译器...

,

您所执行的行为和某个人的代码所依赖的行为越多,该代码的可移植性就越差。在这种情况下,已经有一种实现定义的方法:Curried<A,R>指向整数的指针并在那里进行数学运算。所有人都可以清楚地知道,您所依赖的是特定于实现的行为(或者至少是某些行为可能无法随处移植)。

此外,尽管运行时环境实际上可能是“所有对象都存储在一个大数组中,从大约0开始,以大约内存大小结束,”但这并不是编译时行为。在编译时,您可以获取指向对象的指针并对对象进行指针算术运算。但是,将此类指针仅视为内存中的地址可能会允许用户开始对编译器数据等进行索引。通过将此类对象设为UB,它在编译时就明确禁止使用(// errors cFn(1,1,true); // error! // ~ <-- not a string cFn("",true,false); // error! // ~~~~~ <-- Expected 0-3 arguments,but got 4 cFn("")(1)(false)(true); // error! //~~~~~~~~~~~~~~~ <-- This expression is not callable. 在编译时被明确禁止)。

,

说事物是UB的一个重要原因是允许编译器执行优化。如果您想允许这样的事情,那么您将删除一些优化。正如您所说,这仅在某些小情况下才有用。我要说的是,在大多数情况下,这似乎是一个可行的选择,您应该重新考虑您的设计。

来自以下评论:

我同意,但问题是,尽管我可以重新考虑我的设计,但不能重新考虑其他库的设计。

标准很少用于此类情况。但是它发生了。这就是raster完全有效的原因,即使int *p = 0是指针而p0。这之所以成为标准,是因为它是如此常用,而不是更正确的int。但是总的来说,这是不会发生的,并且有充分的理由。

,

首先,我觉得我们需要弄清楚一些术语,至少关于C。

来自C2011 online draft

  • 未定义的行为-使用非便携式或错误程序构造或错误数据时的行为, 为此,本国际标准没有规定任何要求。可能的不确定行为范围包括完全无法预测地忽略情况 结果,在翻译或程序执行过程中以文件化的特征记录 环境(带有或不带有诊断消息)终止翻译或 执行(发出诊断消息)。

  • 未指定行为-使用未指定的值或本国际标准提供的其他行为 两种或两种以上的可能性,并且不对任何选择的其他要求施加任何其他要求 实例。未指定行为的一个示例是函数的参数的顺序 评估。

  • 实施定义的行为-未指定的行为,其中每个实现都记录了如何做出选择。实现定义的行为的一个示例是高阶位的传播 当有符号整数右移时。

上面的关键点是 unspecified 行为表示语言定义提供了可以从中选择实现的多个值或行为,并且对如何进行选择没有任何进一步的要求。当实现记录了如何做出选择时,未指定的行为变为实现定义的行为。

这意味着对可能被视为实现定义的行为有所限制。

另一个关键点是 undefined 并不意味着非法,而只是意味着不可预测。这意味着您已取消保修,此后发生的任何事情都不由编译器实现负责。未定义行为的一种可能结果是完全按预期工作而没有不良副作用。坦率地说,这是最糟糕的结果,因为这意味着一旦代码或环境中的某些内容发生更改,一切都会崩溃,并且您不知道为什么(在那部电影中出现了几次)。

现在要解决的问题是:

我还知道,在某些体系结构(如我在某处阅读的“分段机”)中,有充分的理由说明行为是不确定的。

这就是为什么无处不在的原因。仍在使用某些体系结构,其中可以将不同的对象存储在不同的内存段中,并且它们的地址中的任何差异都是没有意义的。有太多不同的内存模型和寻址方案,您不能希望定义对所有这些模型都一致的行为(否则定义会非常复杂,难以实现)。

C背后的哲学是最大程度地移植到尽可能多的体系结构中,并且这样做对实现的要求尽可能少。这就是为什么标准算术类型(intfloat等)由值的最小范围定义的原因,它们可以用 minimum precision ,而不是占用的位数。这就是为什么指向不同类型的指针可能具有不同的大小和对齐方式的原因。

对于标准委员会和各种编译器实现者而言,添加使某些行为在此体系结构列表中未定义与在该体系结构列表中未指定的行为的语言将是头痛。这将意味着向诸如gcc之类的编译器添加很多特殊情况的逻辑,这可能使其作为编译器的可靠性降低。