如果两个指针都被强制转换为整数类型,是否将它们与 < undefined 行为进行比较?

Is comparing two pointers with < undefined behavior if they are both cast to an integer type?

假设我有这段代码,可以根据它们的位置以特定顺序将一个内存块复制到另一个内存块:

void *my_memmove(void *dest, const void *src, size_t len)
{
    const unsigned char *s = (const unsigned char *)src;
    unsigned char *d = (unsigned char *)dest;

    if(dest < src)
    {
        /* copy s to d forwards */
    }
    else
    {
        /* copy s to d backwards */
    }

    return dest;
}

如果 srcdest 不指向同一个数组的成员,这是未定义的行为(6.8.5p5).

但是,假设我将这两个指针转换为 uintptr_t 类型:

#include <stdint.h>

void *my_memmove(void *dest, const void *src, size_t len)
{
    const unsigned char *s = (const unsigned char *)src;
    unsigned char *d = (unsigned char *)dest;

    if((uintptr_t)dest < (uintptr_t)src)
    {
        /* copy s to d forwards */
    }
    else
    {
        /* copy s to d backwards */
    }

    return dest;
}

如果它们不是同一数组的成员,这仍然是未定义的行为吗?如果是,我可以通过哪些方式合法地比较内存中的这两个位置?

我看过,但它只处理相等性,不处理其他比较运算符(<>等)。

转换是合法的,但从技术上讲,没有为结果定义任何含义。相反,如果您将指针转换为 void *,然后再转换为 uintptr_t,则定义了轻微的含义:执行反向操作将重现原始指针(或等效的东西)。

特别是,您不能依赖一个整数小于另一个整数这一事实来表示它在内存中的位置较早或地址较低。

uintptr_t (C 2018 7.20.1.4 1) 的规范说它具有 属性 任何有效的 void * 都可以转换为 uintptr_t,然后转换返回 void *,结果将与原始指针比较。

但是,当您将 unsigned char * 转换为 uintptr_t 时,您并未将 void * 转换为 uintptr_t。所以 7.20.1.4 不适用。我们只有6.3.2.3中指针转换的通用定义,其中第5段和第6段说:

An integer may be converted to any pointer type. Except as previously specified [involving zero for null pointers], the result is implementation-defined, might not be correctly aligned, might not point to an entity of the referenced type, and might be a trap representation.

Any pointer type may be converted to an integer type. Except as previously specified [null pointers again], the result is implementation-defined. If the result cannot be represented in the integer type, the behavior is undefined. The result need not be in the range of values of any integer type.

所以这些段落除了告诉您实现文档应该告诉您转换是否有用之外没有任何帮助。毫无疑问,它们在大多数 C 实现中都有。

在您的示例中,您实际上从参数的 void * 开始并将其转换为 unsigned char *,然后再转换为 uintptr_t。所以补救措施很简单:直接从 void *.

转换为 uintptr_t

对于我们有其他指针类型而不是 void * 的情况,则 6.3.2.3 1 很有用:

A pointer to void may be converted to or from a pointer to any object type. A pointer to any object type may be converted to a pointer to void and back again; the result shall compare equal to the original pointer.

因此,与 void * 之间的转换被定义为保留原始指针,因此我们可以将其与从 void *uintptr_t 的转换结合起来:

(uintptr_t) (void *) A < (uintptr_t) (void *) B

由于 (void *) A 必须能够在转换回来时生成原始的 A,并且 (uintptr_t) (void *) A 必须能够生成其 (void *) A,因此 (uintptr_t) (void *) A(uintptr_t) (void *) B 必须不同,如果 AB 不同。

这就是我们从 C 标准中可以说的关于比较的全部内容。从指针转换为整数可能会产生乱序的地址位或其他一些奇怪的情况。例如,它们可能会产生一个包含 16 位段地址和 16 位偏移量的 32 位整数。其中一些整数可能对较低地址具有较高值,而其他整数对较低地址具有较低值。更糟糕的是,同一个地址可能有两种表示形式,因此即使 AB 指的是同一个对象,比较也可能表明“小于”。

没有。每个结果都会产生一个实现定义的值,并且整数的比较总是定义明确的(只要它们的值不是不确定的)。由于这些值是实现定义的,因此比较的结果对于指针而言不需要特别有意义;但是,它必须与整数的属性和实现定义的转换产生的值一致。此外,C 标准表达了这样一种意图,即指针到整数的转换 应该 遵守实现的地址模型,如果遵循这一点,它们将具有一定的意义。请参阅 6.3.2.3 指针下的脚注 67:

The mapping functions for converting a pointer to an integer or an integer to a pointer are intended to be consistent with the addressing structure of the execution environment.

但是,一些当前的编译器 错误地将此视为未定义的行为 ,至少在某些情况下是这样,并且编译器人员正在通过以下概念草率地形式化该选择"provenance",这在内部毫无理由地不一致,而且正在酝酿一场灾难(它可以通过微不足道的更改变得内部一致并且几乎没有问题,这些微不足道的更改对于重要的代码是免费的,但是相信这一点的人东西正在为 Reasons(TM) 而战。

我不了解此事的最新进展,但您可以搜索 "pointer provenance" 并找到文档草案。

比较两个转换为 uintptr_t 的指针不应该 有未定义的行为。它甚至不应该有 未指定的行为。请注意,在转换为 uintptr_t 之前,您应该首先将值转换为 void * 以确保呈现相同。然而,编译器有两个指针被认为不相等的行为,即使它们指向 相同的 地址,同样地,这些指针转换为 uintptr_t 比较彼此不相等(海湾合作委员会 4.7.1 - 4.8.0)。然而,后者是 not 标准所允许的。然而,关于指针来源跟踪的范围*正在进行辩论,这是其中的一部分。

根据C11 footnote 67 is that this is "to be consistent with the addressing structure of the execution environment". The conversion from pointer to integer is implementation-defined and you must check the implementation for the meaning of the cast. For example for GCC, it is defined标准的意图如下:

The result of converting a pointer to an integer or vice versa (C90 6.3.4, C99 and C11 6.3.2.3).

  • A cast from pointer to integer discards most-significant bits if the pointer representation is larger than the integer type, sign-extends 2) if the pointer representation is smaller than the integer type, otherwise the bits are unchanged.

  • A cast from integer to pointer discards most-significant bits if the pointer representation is smaller than the integer type, extends according to the signedness of the integer type if the pointer representation is larger than the integer type, otherwise the bits are unchanged.

  • When casting from pointer to integer and back again, the resulting pointer must reference the same object as the original pointer, otherwise the behavior is undefined. That is, one may not use integer arithmetic to avoid the undefined behavior of pointer arithmetic as proscribed in C99 and C11 6.5.6/8.

例如,在 x86-32、x86-64 和 GCC 上,我们可以确信指针转换为 uintptr_t 的行为是线性偏移按原样转换。


最后一个子句涉及指针来源,即编译器可以跟踪存储在 (u)intptr_t 中的指针的身份,就像它可以跟踪任何其他变量中的指针的身份一样。这是 C 标准完全允许的,因为它只是声明你永远 保证 能够将指向 void 的指针转换为 (u)intptr_t再次回来.

 char foo[4] = "abc";
 char bar[4] = "def";

 if (foo + 4 == bar) {
     printf("%c\n", foo[4]); // undefined behaviour
 }

并且鉴于 foo + 4 比较等于 bar(C 标准允许),您不能 取消引用 foo[4] 因为它确实不是 别名 bar[0]。同样,即使 foo + 4 == bar 你不能做

 uintptr_t foo_as_int = (uintptr_t)(void *)foo;
 if (foo_as_int + 4 == (uintptrt_t)(void *)bar) {
     char *bar_alias = (void *)(foo_as_int + 4);

     printf("%c\n", bar_alias[0]); // undefined behaviour
 }

不能保证将指针转换为 uintptr_t 所产生的数值与所讨论的指针有任何有意义的关系。具有足够存储空间的一致实现可以使第一个指针到整数的转换产生 1,第二个为 2,等等,如果它保留所有已转换指针的列表。

当然,实际实现几乎总是以保留表示的方式执行指针到 uintptr_t 的转换,但是因为标准的作者认为没有必要正式承认一类程序这将在普通平台的普通实现中可移植,有些人将任何此类代码视为 "non-portable" 和 "broken"。这完全违背了标准作者的意图,他们明确表示他们不希望贬低仅符合但不严格符合的程序,但不幸的是,这是一些不需要让客户满意的编译器维护者的普遍态度为了获得报酬。

不,这只是实现定义的行为。但是,如果您使用 == 来确保对象在与 <> 进行比较之前重叠,那么这既不是实现定义的行为也不是未定义的行为。这就是您将如何实施这样的解决方案:

#include <string.h>

void *my_memmove(void *dest, const void *src, size_t len)
{
    const unsigned char *s = src;
    unsigned char *d = dest;
    size_t l;

    if(dest == src)
        goto end;

    /* Check for overlap */
    for( l = 0; l < len; l++ )
    {
        if( s + l == d || s + l == d + len - 1 )
        {
            /* The two objects overlap, so we're allowed to
               use comparison operators. */
            if(s > d)
            {
                /* copy forwards */
                break;
            }
            else /* (s < d) */
            {
                /* copy backwards */
                s += len;
                d += len;
                while(len--)
                {
                    *--d = *--s;
                }
                goto end;
            }
        }
    }

    /* They don't overlap or the source is after
       the destination, so copy forwards */
    while(len--)
    {
        *s++ = *d++;
    }

end:
    return dest;
}