C是否与C ++具有等效的std :: less?


26

我最近在回答一个关于p < qpq是指向不同对象/数组的指针时在C 中执行的未定义行为的问题。这让我想到:C ++ <在这种情况下具有相同(未定义)的行为,但是还提供了标准库模板std::less,该模板保证可以返回与<可以比较指针时相同的东西,并在不能比较时返回一些一致的顺序。

C是否提供具有类似功能的东西,从而可以安全地比较任意指针(相同类型)?我尝试浏览C11标准并没有发现任何东西,但是我在C中的经验比在C ++中小得多,因此我很容易错过一些东西。


1
评论不作进一步讨论;此对话已转移至聊天
塞缪尔·柳

Answers:


20

在具有平面内存模型(基本上所有内容)的实现中,强制转换为uintptr_tJust Work即可。

(但是,请参见应该在64位x86中使用指针比较进行签名还是不签名?以获取有关是否应将指针视为带符号的讨论,包括在C的UB之外的对象之外形成指针的问题。)

但是确实存在具有非平面内存模型的系统,对它们的思考可以帮助解释当前情况,例如C ++对<vs 具有不同的规格std::less


<指向独立对象(在C中为UB)(或至少在某些C ++版本中未指定)的on指针的部分意义是允许使用怪异的机器,包括非平面内存模型。

一个著名的示例是x86-16实模式,其中指针是segment:offset,通过形成20位线性地址(segment << 4) + offset。相同的线性地址可以由多个不同的seg:off组合表示。

std::less怪异ISA上的指针上的C ++ 可能需要昂贵,例如,“标准化” x86-16上的segment:offset以使offset <=15。但是,没有可移植的方法来实现这一点。 规范化uintptr_t(或指针对象的对象表示)所需的操作是特定于实现的。

但是,即使在C ++ std::less必须昂贵的系统上,<也不一定如此。例如,假设一个“大”内存模型,其中一个对象适合一个段,则<可以只比较偏移量部分,而不必理会段部分。(同一对象内的指针将具有相同的段,否则它在C中的UB。C ++ 17更改为仅“未指定”,这可能仍允许跳过规范化并仅比较偏移量。)这是假定所有指向任何部分的指针的对象始终使用相同的seg值,而不进行标准化。这就是您期望ABI需要“大”内存模型而不是“大”内存模型的原因。(请参阅注释中的讨论)。

(例如,这种内存模型的最大对象大小可能为64kiB,但最大总地址空间要大得多,可以容纳许多这样的最大大小的对象。ISOC允许实现对对象大小的限制小于最大值(无符号)size_t可以表示,SIZE_MAX例如,即使在平面内存模型系统上,GNU C也会将最大对象大小限制为最大值,PTRDIFF_MAX以便大小计算可以忽略带符号的溢出。)请参见此答案和注释中的讨论。

如果要允许对象大于段,则需要一个“巨大”的内存模型,该模型必须担心在p++遍历数组或执行索引/指针算术时指针的偏移量部分溢出。这会导致各处代码变慢,但可能意味着这p < q将适用于指向不同对象的指针,因为针对“巨大”内存模型的实现通常会选择始终将所有指针标准化。看到什么是近,远和巨大的指针?-某些用于x86实模式的真正C编译器确实提供了针对“巨大”模型进行编译的选项,其中除非另外声明,否则所有指针均默认为“巨大”。

x86实模式分段不是唯一可能的非平面内存模型,它只是一个有用的具体示例,它说明了C / C ++实现如何处理它。在现实生活中,实现通过farvs. near指针的概念扩展了ISO C ,允许程序员选择何时可以相对于某些公共数据段仅存储/传递16位偏移量部分。

但是,纯ISO C实现必须在小型内存模型(相同的64kiB中具有16位指针的代码除外)之间选择,或者在所有指针均为32位的情况下选择大内存或大内存。有些循环可以通过仅增加偏移量部分来进行优化,但是指针对象无法进行优化以使其更小。


如果您知道任何给定实现的魔术操作是什么,则可以用纯C实现。问题在于,不同的系统使用不同的寻址方式,并且任何可移植的宏都无法对细节进行参数设置。

也许不是:它可能涉及从特殊的段表中查找某些内容,例如x86保护模式而不是实模式,其中地址的段部分是索引,而不是左移的值。您可以在保护模式下设置部分重叠的段,地址的段选择器部分甚至不必与相应段基地址的顺序相同。如果GDT和/或LDT没有映射到进程中的可读页面,则在x86保护模式下从seg:off指针获取线性地址可能涉及系统调用。

(当然,x86的主流OS使用平面内存模型,因此段基数始终为0(使用fsgs段的线程本地存储除外),并且仅32位或64位“偏移”部分用作指针。 )

您可以手动添加用于各种特定平台的代码,例如默认情况下假定为flat,或#ifdef用于检测x86实模式的内容并将其拆分uintptr_t为16位的一半,seg -= off>>4; off &= 0xf;然后将这些部分组合回32位的数字。


如果细分不相等,为什么会是UB?
橡子

@橡子:意思是说相反。固定。指向同一对象的指针将具有相同的段,否则为UB。
彼得·科德斯

但是,无论如何,您为什么认为它是UB?(逻辑是否正确,实际上我也没有注意到)
Acorn

p < q如果UB指向不同的对象,是不是?我知道p - q
彼得·科德斯

1
@Acorn:无论如何,我没有看到一种在没有UB的程序中会生成别名(不同的seg:off,相同的线性地址)的机制。因此,这并不意味着编译器必须竭尽所能避免这种情况。对对象的每次访问都使用该对象的seg值和一个偏移量,该偏移量> =该对象起始段内的偏移量。C使UB可以在指向不同对象的指针之间做很多事情,包括类似之类的东西tmp = a-b,然后再b[tmp]进行access a[0]。有关分段指针别名的讨论是说明为什么选择设计有意义的一个很好的例子。
彼得·科德斯

17

曾经尝试找到一种解决方法,但确实找到了一种适用于重叠对象的解决方案,并且在大多数其他情况下,假设编译器执行“常规”操作。

您可以首先实现如何在没有中间副本的情况下在标准C中实现记忆?然后,如果这不起作用,则强制转换为uintptr(一个包装类型,uintptr_t或者unsigned long long取决于是否uintptr_t可用),并获得最可能准确的结果(尽管无论如何这都无关紧要):

#include <stdint.h>
#ifndef UINTPTR_MAX
typedef unsigned long long uintptr;
#else
typedef uintptr_t uintptr;
#endif

int pcmp(const void *p1, const void *p2, size_t len)
{
    const unsigned char *s1 = p1;
    const unsigned char *s2 = p2;
    size_t l;

    /* Check for overlap */
    for( l = 0; l < len; l++ )
    {
        if( s1 + l == s2 || s1 + l == s2 + len - 1 )
        {
            /* The two objects overlap, so we're allowed to
               use comparison operators. */
            if(s1 > s2)
                return 1;
            else if (s1 < s2)
                return -1;
            else
                return 0;
        }
    }

    /* No overlap so the result probably won't really matter.
       Cast the result to `uintptr` and hope the compiler
       does the "usual" thing */
    if((uintptr)s1 > (uintptr)s2)
        return 1;
    else if ((uintptr)s1 < (uintptr)s2)
        return -1;
    else
        return 0;
}

5

C是否提供具有类似功能的东西,从而可以安全地比较任意指针。

没有


首先让我们只考虑对象指针函数指针带来了其他一系列问题。

2个指针p1, p2可以具有不同的编码,并且指向相同的地址,因此p1 == p2即使memcmp(&p1, &p2, sizeof p1)不为0也是如此。此类架构很少见。

这些指针到的转换uintptr_t不需要相同的整数结果导致(uintptr_t)p1 != (uinptr_t)p2

(uintptr_t)p1 < (uinptr_t)p2 本身就是很好的法律法规,可能无法提供所希望的功能。


如果代码确实需要比较不相关的指针,请形成一个辅助函数less(const void *p1, const void *p2)并在那里执行平台特定的代码。

也许:

// return -1,0,1 for <,==,> 
int ptrcmp(const void *c1, const void *c1) {
  // Equivalence test works on all platforms
  if (c1 == c2) {
    return 0;
  }
  // At this point, we know pointers are not equivalent.
  #ifdef UINTPTR_MAX
    uintptr_t u1 = (uintptr_t)c1;
    uintptr_t u2 = (uintptr_t)c2;
    // Below code "works" in that the computation is legal,
    //   but does it function as desired?
    // Likely, but strange systems lurk out in the wild. 
    // Check implementation before using
    #if tbd
      return (u1 > u2) - (u1 < u2);
    #else
      #error TBD code
    #endif
  #else
    #error TBD code
  #endif 
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.