数组外的指针比较的基本原理是UB

因此,标准(参考N1570 )说明了以下关于比较指针的内容:

C99 6.5.8 / 5关系运算符

比较两个指针时,结果取决于指向的对象的地址空间中的相对位置。 … [在聚合中剪切明显的比较定义] … 在所有其他情况下,行为是未定义的。

这个UB实例的基本原理是什么,而不是指定(例如)转换为intptr_t并进行比较?

是否存在一些机器架构,其中指针的合理总排序难以构建? 是否存在一些优化或分析,不受限制的指针比较会阻碍?

这个问题的删除答案提到这块UB允许跳过段寄存器的比较并且仅比较偏移。 保存特别有价值吗?

(同样删除的答案,以及此处的答案,请注意,在C ++中, std::less等是实现指针总顺序所必需的,无论普通比较运算符是否执行。)

ub邮件列表讨论中的各种评论<不是指针的总命令的理由? 强烈暗示分段架构是其中的原因。 包括以下评论, 1 :

另外,我认为核心语言应该简单地认识到这些天所有机器都具有扁平内存模型的事实。

和2 :

然后我们可能需要一种新类型,它可以保证从指针转换时的总顺序(例如,在分段体系结构中,转换需要获取段寄存器的地址并添加存储在指针中的偏移量)。

和3 :

指针虽然历史上并非完全有序,但对于现有的所有系统来说实际上都是如此,除了委员会的象牙塔之外,所以重点是没有实际意义。

和4 :

但是,即使分段的体系结构(尽管它不太可能)确实回来了,仍然必须解决排序问题,因为需要std :: less来完全排序指针。 我只想让operator <成为该属性的替代拼写。

为什么其他人都假装受苦(我的意思是假装,因为除了委员会的一小部分人之外,人们已经假设指针完全按照运算符<)来满足当前一些不存在的架构的理论需求?

ub邮件列表的评论趋势相反,FUZxxl指出支持DOS是不支持完全有序指针的原因。

更新

Annotated C ++ Reference Manual ( ARM )也支持这一点,它说这是由于在分段体系结构上支持这一点的负担:

在分段体系结构上,表达式可能不会被评估为false […]这就解释了为什么指针的加法,减法和比较仅针对指向数组的指针和超出结尾的一个元素进行定义。 […]具有非分段地址空间的机器的用户开发了成语,但是,除了特殊的努力之外,提到超出arrays末尾的元素不能移植到分段体系结构[…]允许[…]费用昂贵,几乎没有用处。

8086是一个具有16位寄存器和20位地址空间的处理器。 为了应对寄存器中缺少的位,存在一组段寄存器 。 在内存访问时,解除引用的地址计算如下:

 address = 16 * segment + register 

请注意,除其他外,地址通常有多种表示方式。 比较两个任意地址是繁琐的,因为编译器必须首先规范化两个地址,然后比较规范化的地址。

许多编译器指定(在可能的内存模型中),在进行指针运算时,段部分将保持不变。 这有几个后果:

  • 对象的大小最多为64 kB
  • 对象中的所有地址都具有相同的段部分
  • 比较对象中的地址可以通过比较寄存器部分来完成; 这可以在一条指令中完成

这种快速比较当然仅在指针从相同的基地址派生时才有效,这是C标准仅在两个指针指向同一对象时定义指针比较的原因之一。

如果要对所有指针进行有序的比较,请考虑首先将指针转换为uintptr_t值。

我相信它是未定义的,因此C可以在架构上运行,实际上,“智能指针”在硬件中实现,通过各种检查确保指针永远不会意外地指向它们被定义引用的内存区域之外。 我从来没有亲自使用过这样的机器,但考虑它们的方法是计算无效指针正如禁用0那样被禁止; 您可能会遇到终止程序的运行时exception。 此外,禁止的是计算指针,你甚至不必取消引用它来获得exception。

是的,我相信这个定义也最终允许在旧的8086代码中更有效地比较偏移寄存器,但这不是唯一的原因。

是的,这些受保护指针体系结构之一的编译器理论上可以通过转换为无符号或等效的方式来实现“禁止”比较,但(a)这样做的效率可能会大大降低,而且(b)这将是一个肆无忌惮的故意规避架构的预期保护,至少部分架构的C程序员可能希望启用(未禁用)。

从历史上看,该行为调用未定义行为意味着任何使用此类行为的程序都可以正确地仅在那些为该行为定义满足其要求的行为定义的实现上 。 指定调用未定义行为的操作并不意味着使用此类操作的程序应被视为“非法”,而是意图允许C在无法有效的平台上运行不需要此类操作的程序支持他们。

通常,期望编译器将输出指令序列,该指令序列将在标准所要求的情况下最有效地执行指示的动作,并执行在其他情况下发生的任何指令序列,或者将输出序列在这种情况下,其行为被认为在某种程度上比自然序列更“有用”。 如果某个操作可能触发硬件陷阱,或者在某些情况下触发OS陷阱可能被认为优于执行“自然”指令序列,并且陷阱可能导致C编译器控制之外的行为,标准没有要求。 因此,这种情况被标记为“未定义的行为”。

正如其他人所指出的那样,有些平台的p1 < p2 ,对于不相关的指针p1和p2,可以保证产生0或1,但是最有效的方法是比较p1和p2,它们可以在由标准可能不会维持p1 < p2 || p2 > p2 || p1 != p2的通常期望 p1 < p2 || p2 > p2 || p1 != p2 p1 < p2 || p2 > p2 || p1 != p2 。 如果为这样的平台编写的程序知道它永远不会故意比较不相关的指针(暗示任何这样的比较将代表程序错误),那么进行压力测试或故障排除构建生成可以捕获任何此类比较的代码可能会有所帮助。 标准允许此类实现的唯一方法是进行此类比较未定义行为。

直到最近,特定动作将调用标准未定义的行为这一事实通常只会给试图在行动会产生不良后果的平台上编写代码的人带来困难。 此外,在一个行动只会产生不良后果的平台上,如果编译器不愿意这样做,那么程序员通常会接受这样一种明智的行为。

如果接受以下观念:

  1. 该标准的作者期望不相关指针之间的比较在这些平台上有用,并且只有那些比较相关指针的最自然方法也适用于不相关指针的平台,以及

  2. 存在比较不相关指针会有问题的平台

然后,标准将完全意义上的无关指针比较视为未定义行为。 如果他们预料到即使是针对所有指针定义不相交的全局排名的平台的编译器也可能会使无关指针比较否定时间和因果关系的规律(例如:

 int needle_in_haystack(char const *hs_base, int hs_size, char *needle) { return needle >= hs_base && needle < hs_base+hs_size; } 

编译器可能会推断程序永远不会收到任何会导致needle_in_haystack被赋予无关指针的输入,并且任何只有在程序接收到这样的输入时才相关的代码才可以被删除)我认为它们会以不同的方式指定事物。 编译器编写者可能认为编写needle_in_haystack的正确方法是:

 int needle_in_haystack(char const *hs_base, int hs_size, char *needle) { for (int i=0; i 

因为他们的编译器会识别循环正在做什么,并且还认识到它在不相关的指针比较工作的平台上运行,因此生成与旧编译器为早期声明的公式生成的相同的机器代码。 至于是否更好地要求编译器提供一种方法来指定类似于前一版本的代码应该在支持它的平台上合理地使用,或者拒绝对那些不支持它的编译进行编译,或者更好地要求程序员打算使用前一种语义应该写后者并希望优化者把它变成有用的东西,我把它留给读者的判断。